mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
[WIP] Business
This commit is contained in:
parent
16c226c801
commit
46881c65ca
@ -395,13 +395,13 @@ public enum ChatSearchDomain: Equatable {
|
||||
public enum ChatLocation: Equatable {
|
||||
case peer(id: PeerId)
|
||||
case replyThread(message: ChatReplyThreadMessage)
|
||||
case feed(id: Int32)
|
||||
case customChatContents
|
||||
}
|
||||
|
||||
public extension ChatLocation {
|
||||
var normalized: ChatLocation {
|
||||
switch self {
|
||||
case .peer, .feed:
|
||||
case .peer, .customChatContents:
|
||||
return self
|
||||
case let .replyThread(message):
|
||||
return .replyThread(message: message.normalized)
|
||||
@ -936,7 +936,8 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeBusinessHoursSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController
|
||||
func makeGreetingMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController
|
||||
func makeQuickReplySetupScreen(context: AccountContext) -> ViewController
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>
|
||||
|
@ -744,6 +744,7 @@ public enum ChatControllerSubject: Equatable {
|
||||
case scheduledMessages
|
||||
case pinnedMessages(id: EngineMessage.Id?)
|
||||
case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo)
|
||||
case customChatContents(contents: ChatCustomContentsProtocol)
|
||||
|
||||
public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool {
|
||||
switch lhs {
|
||||
@ -771,6 +772,12 @@ public enum ChatControllerSubject: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .customChatContents(lhsValue):
|
||||
if case let .customChatContents(rhsValue) = rhs, lhsValue === rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1050,7 +1057,23 @@ public enum ChatHistoryListSource {
|
||||
}
|
||||
|
||||
case `default`
|
||||
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: Quote?, loadMore: (() -> Void)?)
|
||||
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, loadMore: (() -> Void)?)
|
||||
}
|
||||
|
||||
public enum ChatCustomContentsKind: Equatable {
|
||||
case greetingMessageInput
|
||||
case awayMessageInput
|
||||
case quickReplyMessageInput(shortcut: String)
|
||||
}
|
||||
|
||||
public protocol ChatCustomContentsProtocol: AnyObject {
|
||||
var kind: ChatCustomContentsKind { get }
|
||||
var messages: Signal<[Message], NoError> { get }
|
||||
var messageLimit: Int? { get }
|
||||
|
||||
func enqueueMessages(messages: [EnqueueMessage])
|
||||
func deleteMessages(ids: [EngineMessage.Id])
|
||||
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool)
|
||||
}
|
||||
|
||||
public enum ChatHistoryListDisplayHeaders {
|
||||
@ -1069,7 +1092,7 @@ public protocol ChatControllerInteractionProtocol: AnyObject {
|
||||
|
||||
public enum ChatHistoryNodeHistoryState: Equatable {
|
||||
case loading
|
||||
case loaded(isEmpty: Bool)
|
||||
case loaded(isEmpty: Bool, hasReachedLimits: Bool)
|
||||
}
|
||||
|
||||
public protocol ChatHistoryListNode: ListView {
|
||||
|
@ -36,8 +36,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
|
||||
return .peer(peerId)
|
||||
case let .replyThread(replyThreaMessage):
|
||||
return .peer(replyThreaMessage.peerId)
|
||||
case let .feed(id):
|
||||
return .feed(id)
|
||||
case .customChatContents:
|
||||
return .custom
|
||||
}
|
||||
case let .singleMessage(id):
|
||||
return .peer(id.peerId)
|
||||
|
@ -26,6 +26,7 @@ import TextNodeWithEntities
|
||||
import ComponentFlow
|
||||
import EmojiStatusComponent
|
||||
import AvatarVideoNode
|
||||
import AppBundle
|
||||
|
||||
public enum ChatListItemContent {
|
||||
public struct ThreadInfo: Equatable {
|
||||
@ -89,6 +90,16 @@ public enum ChatListItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CustomMessageListData: Equatable {
|
||||
public var commandPrefix: String?
|
||||
public var messageCount: Int?
|
||||
|
||||
public init(commandPrefix: String?, messageCount: Int?) {
|
||||
self.commandPrefix = commandPrefix
|
||||
self.messageCount = messageCount
|
||||
}
|
||||
}
|
||||
|
||||
public struct PeerData {
|
||||
public var messages: [EngineMessage]
|
||||
public var peer: EngineRenderedPeer
|
||||
@ -112,6 +123,7 @@ public enum ChatListItemContent {
|
||||
public var requiresPremiumForMessaging: Bool
|
||||
public var displayAsTopicList: Bool
|
||||
public var tags: [Tag]
|
||||
public var customMessageListData: CustomMessageListData?
|
||||
|
||||
public init(
|
||||
messages: [EngineMessage],
|
||||
@ -135,7 +147,8 @@ public enum ChatListItemContent {
|
||||
storyState: StoryState?,
|
||||
requiresPremiumForMessaging: Bool,
|
||||
displayAsTopicList: Bool,
|
||||
tags: [Tag]
|
||||
tags: [Tag],
|
||||
customMessageListData: CustomMessageListData? = nil
|
||||
) {
|
||||
self.messages = messages
|
||||
self.peer = peer
|
||||
@ -159,6 +172,7 @@ public enum ChatListItemContent {
|
||||
self.requiresPremiumForMessaging = requiresPremiumForMessaging
|
||||
self.displayAsTopicList = displayAsTopicList
|
||||
self.tags = tags
|
||||
self.customMessageListData = customMessageListData
|
||||
}
|
||||
}
|
||||
|
||||
@ -1153,6 +1167,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let inputActivitiesNode: ChatListInputActivitiesNode
|
||||
let dateNode: TextNode
|
||||
var dateStatusIconNode: ASImageNode?
|
||||
var dateDisclosureIconView: UIImageView?
|
||||
let separatorNode: ASDisplayNode
|
||||
let statusNode: ChatListStatusNode
|
||||
let badgeNode: ChatListBadgeNode
|
||||
@ -1587,7 +1602,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
if let peer = peer {
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if peer.id.isReplies {
|
||||
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
|
||||
} else if peer.id.isReplies {
|
||||
overrideImage = .repliesIcon
|
||||
} else if peer.id.isAnonymousSavedMessages {
|
||||
overrideImage = .anonymousSavedMessagesIcon
|
||||
@ -2021,15 +2037,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let enableChatListPhotos = true
|
||||
|
||||
let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
|
||||
|
||||
var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
|
||||
let avatarLeftInset: CGFloat
|
||||
if item.interaction.isInlineMode {
|
||||
avatarLeftInset = 12.0
|
||||
} else if !useChatListLayout {
|
||||
avatarLeftInset = 50.0
|
||||
} else {
|
||||
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
|
||||
avatarDiameter = 40.0
|
||||
avatarLeftInset = 18.0 + avatarDiameter
|
||||
} else {
|
||||
if item.interaction.isInlineMode {
|
||||
avatarLeftInset = 12.0
|
||||
} else if !useChatListLayout {
|
||||
avatarLeftInset = 50.0
|
||||
} else {
|
||||
avatarLeftInset = 18.0 + avatarDiameter
|
||||
}
|
||||
}
|
||||
|
||||
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
|
||||
@ -2083,7 +2105,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
hideAuthor = true
|
||||
}
|
||||
|
||||
let attributedText: NSAttributedString
|
||||
var attributedText: NSAttributedString
|
||||
var hasDraft = false
|
||||
|
||||
var inlineAuthorPrefix: String?
|
||||
@ -2401,6 +2423,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
attributedText = composedString
|
||||
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, let commandPrefix = customMessageListData.commandPrefix {
|
||||
let mutableAttributedText = NSMutableAttributedString(attributedString: attributedText)
|
||||
let boldTextFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
|
||||
mutableAttributedText.insert(NSAttributedString(string: commandPrefix + " ", font: boldTextFont, textColor: theme.titleColor), at: 0)
|
||||
attributedText = mutableAttributedText
|
||||
}
|
||||
|
||||
if !ignoreForwardedIcon {
|
||||
if case .savedMessagesChats = item.chatListLocation {
|
||||
displayForwardedIcon = false
|
||||
@ -2548,7 +2577,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
switch contentData {
|
||||
case let .chat(itemPeer, threadInfo, _, _, _, _, _):
|
||||
if let threadInfo = threadInfo {
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
|
||||
if customMessageListData.commandPrefix != nil {
|
||||
titleAttributedString = nil
|
||||
} else {
|
||||
if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) {
|
||||
let textColor: UIColor
|
||||
if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
textColor = theme.secretTitleColor
|
||||
} else {
|
||||
textColor = theme.titleColor
|
||||
}
|
||||
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
|
||||
}
|
||||
}
|
||||
} else if let threadInfo = threadInfo {
|
||||
titleAttributedString = NSAttributedString(string: threadInfo.info.title, font: titleFont, textColor: theme.titleColor)
|
||||
} else if let message = messages.last, case let .user(author) = message.author, displayAsMessage {
|
||||
titleAttributedString = NSAttributedString(string: author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer.user(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: titleFont, textColor: theme.titleColor)
|
||||
@ -2587,7 +2630,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
case let .peer(peerData):
|
||||
topIndex = peerData.messages.first?.index
|
||||
}
|
||||
if let topIndex {
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
|
||||
if let messageCount = customMessageListData.messageCount {
|
||||
dateText = "\(messageCount)"
|
||||
} else {
|
||||
dateText = " "
|
||||
}
|
||||
} else if let topIndex {
|
||||
var t = Int(topIndex.timestamp)
|
||||
var timeinfo = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
@ -2754,7 +2803,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
switch item.content {
|
||||
case let .peer(peerData):
|
||||
if let peer = peerData.messages.last?.author {
|
||||
if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
|
||||
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
|
||||
currentCredibilityIconContent = nil
|
||||
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
|
||||
currentCredibilityIconContent = nil
|
||||
} else if peer.isScam {
|
||||
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
|
||||
@ -2776,7 +2827,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
break
|
||||
}
|
||||
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
|
||||
if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
|
||||
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
|
||||
currentCredibilityIconContent = nil
|
||||
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
|
||||
currentCredibilityIconContent = nil
|
||||
} else if peer.isScam {
|
||||
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
|
||||
@ -3066,16 +3119,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
animateContent = true
|
||||
}
|
||||
|
||||
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: " ", font: titleFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSpacing: CGFloat = -1.0
|
||||
let authorSpacing: CGFloat = -3.0
|
||||
var itemHeight: CGFloat = 8.0 * 2.0 + 1.0
|
||||
itemHeight -= 21.0
|
||||
itemHeight += titleLayout.size.height
|
||||
itemHeight += measureLayout.size.height * 3.0
|
||||
itemHeight += titleSpacing
|
||||
itemHeight += authorSpacing
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
|
||||
itemHeight += measureLayout.size.height * 2.0
|
||||
itemHeight += 22.0
|
||||
} else {
|
||||
itemHeight += titleLayout.size.height
|
||||
itemHeight += measureLayout.size.height * 3.0
|
||||
itemHeight += titleSpacing
|
||||
itemHeight += authorSpacing
|
||||
}
|
||||
|
||||
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0))
|
||||
|
||||
@ -3466,7 +3524,32 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let _ = mentionBadgeApply(animateBadges, true)
|
||||
let _ = onlineApply(animateContent && animateOnline)
|
||||
|
||||
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size))
|
||||
var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
|
||||
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil {
|
||||
dateFrame.origin.x -= 10.0
|
||||
|
||||
let dateDisclosureIconView: UIImageView
|
||||
if let current = strongSelf.dateDisclosureIconView {
|
||||
dateDisclosureIconView = current
|
||||
} else {
|
||||
dateDisclosureIconView = UIImageView(image: UIImage(bundleImageName: "Item List/DisclosureArrow")?.withRenderingMode(.alwaysTemplate))
|
||||
strongSelf.dateDisclosureIconView = dateDisclosureIconView
|
||||
strongSelf.mainContentContainerNode.view.addSubview(dateDisclosureIconView)
|
||||
}
|
||||
dateDisclosureIconView.tintColor = item.presentationData.theme.list.disclosureArrowColor
|
||||
let iconScale: CGFloat = 0.7
|
||||
if let image = dateDisclosureIconView.image {
|
||||
let imageSize = CGSize(width: floor(image.size.width * iconScale), height: floor(image.size.height * iconScale))
|
||||
let iconFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - imageSize.width + 4.0, y: floorToScreenPixels(dateFrame.midY - imageSize.height * 0.5)), size: imageSize)
|
||||
dateDisclosureIconView.frame = iconFrame
|
||||
}
|
||||
} else if let dateDisclosureIconView = strongSelf.dateDisclosureIconView {
|
||||
strongSelf.dateDisclosureIconView = nil
|
||||
dateDisclosureIconView.removeFromSuperview()
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.dateNode, frame: dateFrame)
|
||||
|
||||
var statusOffset: CGFloat = 0.0
|
||||
if let dateIconImage {
|
||||
@ -3997,6 +4080,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
|
||||
if customMessageListData.messageCount != nil {
|
||||
strongSelf.separatorNode.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: itemHeight)))
|
||||
let backgroundColor: UIColor
|
||||
let highlightedBackgroundColor: UIColor
|
||||
@ -4012,7 +4101,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor
|
||||
}
|
||||
} else {
|
||||
backgroundColor = theme.itemBackgroundColor
|
||||
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
|
||||
backgroundColor = .clear
|
||||
} else {
|
||||
backgroundColor = theme.itemBackgroundColor
|
||||
}
|
||||
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ public extension ChatLocation {
|
||||
return peerId
|
||||
case let .replyThread(replyThreadMessage):
|
||||
return replyThreadMessage.peerId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ public extension ChatLocation {
|
||||
return nil
|
||||
case let .replyThread(replyThreadMessage):
|
||||
return replyThreadMessage.threadId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -605,7 +605,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
||||
case let .replyThread(message):
|
||||
peerIdValue = message.peerId
|
||||
threadIdValue = message.threadId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
if peerIdValue == context.account.peerId, let customTag {
|
||||
|
@ -55,6 +55,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||
if self.alwaysPlain {
|
||||
neighbors.top = .sameSection(alwaysPlain: false)
|
||||
}
|
||||
if self.alwaysPlain {
|
||||
neighbors.bottom = .sameSection(alwaysPlain: false)
|
||||
}
|
||||
let (layout, apply) = node.asyncLayout()(self, params, neighbors)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
@ -83,6 +86,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||
if self.alwaysPlain {
|
||||
neighbors.top = .sameSection(alwaysPlain: false)
|
||||
}
|
||||
if self.alwaysPlain {
|
||||
neighbors.bottom = .sameSection(alwaysPlain: false)
|
||||
}
|
||||
let (layout, apply) = makeLayout(self, params, neighbors)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
|
@ -4,7 +4,7 @@ import SwiftSignalKit
|
||||
public enum ChatLocationInput {
|
||||
case peer(peerId: PeerId, threadId: Int64?)
|
||||
case thread(peerId: PeerId, threadId: Int64, data: Signal<MessageHistoryViewExternalInput, NoError>)
|
||||
case feed(id: Int32, data: Signal<MessageHistoryViewExternalInput, NoError>)
|
||||
case customChatContents
|
||||
}
|
||||
|
||||
public extension ChatLocationInput {
|
||||
@ -14,7 +14,7 @@ public extension ChatLocationInput {
|
||||
return peerId
|
||||
case let .thread(peerId, _, _):
|
||||
return peerId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -25,7 +25,7 @@ public extension ChatLocationInput {
|
||||
return threadId
|
||||
case let .thread(_, threadId, _):
|
||||
return threadId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -639,6 +639,10 @@ public extension MessageAttribute {
|
||||
|
||||
public struct MessageGroupInfo: Equatable {
|
||||
public let stableId: UInt32
|
||||
|
||||
public init(stableId: UInt32) {
|
||||
self.stableId = stableId
|
||||
}
|
||||
}
|
||||
|
||||
public final class Message {
|
||||
|
@ -1101,7 +1101,7 @@ public final class MessageHistoryView {
|
||||
self.topTaggedMessages = []
|
||||
self.additionalData = []
|
||||
self.isLoading = isLoading
|
||||
self.isLoadingEarlier = true
|
||||
self.isLoadingEarlier = false
|
||||
self.isAddedToChatList = false
|
||||
self.peerStoryStats = [:]
|
||||
}
|
||||
|
@ -2997,6 +2997,8 @@ final class PostboxImpl {
|
||||
private func internalTransaction<T>(_ f: (Transaction) -> T) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) {
|
||||
let _ = self.isInTransaction.swap(true)
|
||||
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
self.valueBox.begin()
|
||||
let transaction = Transaction(queue: self.queue, postbox: self)
|
||||
self.afterBegin(transaction: transaction)
|
||||
@ -3005,6 +3007,12 @@ final class PostboxImpl {
|
||||
transaction.disposed = true
|
||||
self.valueBox.commit()
|
||||
|
||||
let endTime = CFAbsoluteTimeGetCurrent()
|
||||
let transactionDuration = endTime - startTime
|
||||
if transactionDuration > 0.1 {
|
||||
postboxLog("Postbox transaction took \(transactionDuration * 1000.0) ms")
|
||||
}
|
||||
|
||||
let _ = self.isInTransaction.swap(false)
|
||||
|
||||
if let currentUpdatedState = self.currentUpdatedState {
|
||||
@ -3079,7 +3087,7 @@ final class PostboxImpl {
|
||||
switch chatLocation {
|
||||
case let .peer(peerId, threadId):
|
||||
return .single((.peer(peerId: peerId, threadId: threadId), false))
|
||||
case .thread(_, _, let data), .feed(_, let data):
|
||||
case .thread(_, _, let data):
|
||||
return Signal { subscriber in
|
||||
var isHoleFill = false
|
||||
return (data
|
||||
@ -3089,6 +3097,9 @@ final class PostboxImpl {
|
||||
return (.external(value), wasHoleFill)
|
||||
}).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
}
|
||||
case .customChatContents:
|
||||
assert(false)
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch
|
||||
return ActionDisposable {
|
||||
disposable.dispose()
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
subscriber.putNext(([], true))
|
||||
|
||||
return ActionDisposable {
|
||||
|
@ -179,7 +179,7 @@ private func wrappedHistoryViewAdditionalData(chatLocation: ChatLocationInput, a
|
||||
result.append(.peerChatState(peerId))
|
||||
}
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
return result
|
||||
@ -1839,7 +1839,7 @@ public final class AccountViewTracker {
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation)
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -1852,7 +1852,7 @@ public final class AccountViewTracker {
|
||||
peerId = peerIdValue
|
||||
case let .thread(peerIdValue, _, _):
|
||||
peerId = peerIdValue
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
peerId = nil
|
||||
}
|
||||
if let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
|
@ -107,6 +107,46 @@ public func stringForMonth(strings: PresentationStrings, month: Int32, ofYear ye
|
||||
}
|
||||
}
|
||||
|
||||
private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String {
|
||||
switch index {
|
||||
case 0:
|
||||
return strings.Month_ShortJanuary
|
||||
case 1:
|
||||
return strings.Month_ShortFebruary
|
||||
case 2:
|
||||
return strings.Month_ShortMarch
|
||||
case 3:
|
||||
return strings.Month_ShortApril
|
||||
case 4:
|
||||
return strings.Month_ShortMay
|
||||
case 5:
|
||||
return strings.Month_ShortJune
|
||||
case 6:
|
||||
return strings.Month_ShortJuly
|
||||
case 7:
|
||||
return strings.Month_ShortAugust
|
||||
case 8:
|
||||
return strings.Month_ShortSeptember
|
||||
case 9:
|
||||
return strings.Month_ShortOctober
|
||||
case 10:
|
||||
return strings.Month_ShortNovember
|
||||
case 11:
|
||||
return strings.Month_ShortDecember
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public func stringForCompactDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
|
||||
var t: time_t = time_t(timestamp)
|
||||
var timeinfo: tm = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
//TODO:localize
|
||||
return "\(shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)) \(timeinfo.tm_mday) \(monthAtIndex(Int(timeinfo.tm_mon), strings: strings))"
|
||||
}
|
||||
|
||||
public enum RelativeTimestampFormatDay {
|
||||
case today
|
||||
case yesterday
|
||||
@ -362,37 +402,6 @@ public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationSt
|
||||
}
|
||||
}
|
||||
|
||||
private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String {
|
||||
switch index {
|
||||
case 0:
|
||||
return strings.Month_GenJanuary
|
||||
case 1:
|
||||
return strings.Month_GenFebruary
|
||||
case 2:
|
||||
return strings.Month_GenMarch
|
||||
case 3:
|
||||
return strings.Month_GenApril
|
||||
case 4:
|
||||
return strings.Month_GenMay
|
||||
case 5:
|
||||
return strings.Month_GenJune
|
||||
case 6:
|
||||
return strings.Month_GenJuly
|
||||
case 7:
|
||||
return strings.Month_GenAugust
|
||||
case 8:
|
||||
return strings.Month_GenSeptember
|
||||
case 9:
|
||||
return strings.Month_GenOctober
|
||||
case 10:
|
||||
return strings.Month_GenNovember
|
||||
case 11:
|
||||
return strings.Month_GenDecember
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
|
||||
let difference = timestamp - relativeTimestamp
|
||||
if difference < 60 {
|
||||
|
@ -818,8 +818,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
if !isBroadcastChannel {
|
||||
hasAvatar = true
|
||||
} else if case .feed = item.chatLocation {
|
||||
hasAvatar = true
|
||||
}
|
||||
}
|
||||
} else if incoming {
|
||||
@ -844,8 +842,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
} else if incoming {
|
||||
hasAvatar = true
|
||||
}
|
||||
case .feed:
|
||||
hasAvatar = true
|
||||
case .customChatContents:
|
||||
hasAvatar = false
|
||||
}
|
||||
|
||||
if hasAvatar {
|
||||
@ -1445,6 +1443,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0 + imageBottomPadding), size: dateAndStatusSize)
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
dateAndStatusApply(animation)
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
strongSelf.dateAndStatusNode.isHidden = true
|
||||
}
|
||||
|
||||
if needsReplyBackground {
|
||||
if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||
|
@ -675,7 +675,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
|
||||
if case let .linear(_, bottom) = position {
|
||||
if case .customChatContents = associatedData.subject {
|
||||
} else if case let .linear(_, bottom) = position {
|
||||
switch bottom {
|
||||
case .None, .Neighbour(_, .footer, _):
|
||||
if message.adAttribute == nil {
|
||||
|
@ -1446,8 +1446,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
if !isBroadcastChannel {
|
||||
hasAvatar = incoming
|
||||
} else if case .feed = item.chatLocation {
|
||||
hasAvatar = true
|
||||
} else if case .customChatContents = item.chatLocation {
|
||||
hasAvatar = false
|
||||
}
|
||||
}
|
||||
} else if incoming {
|
||||
@ -2072,7 +2072,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
maximumNodeWidth = size.width
|
||||
|
||||
if mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment {
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
} else if mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment {
|
||||
let message = item.content.firstMessage
|
||||
|
||||
var edited = false
|
||||
|
@ -252,7 +252,10 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch position {
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
@ -267,6 +270,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
|
@ -112,8 +112,11 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
incoming = false
|
||||
}
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
@ -127,6 +130,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
|
||||
|
@ -201,21 +201,25 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN
|
||||
}
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
default:
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
if item.message.flags.contains(.Failed) {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
|
||||
|
@ -321,8 +321,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
|
||||
|
||||
if !isBroadcastChannel {
|
||||
hasAvatar = true
|
||||
} else if case .feed = item.chatLocation {
|
||||
hasAvatar = true
|
||||
} else if case .customChatContents = item.chatLocation {
|
||||
hasAvatar = false
|
||||
}
|
||||
}
|
||||
} else if incoming {
|
||||
|
@ -949,6 +949,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
}
|
||||
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
strongSelf.dateAndStatusNode.isHidden = true
|
||||
}
|
||||
|
||||
if let videoNode = strongSelf.videoNode {
|
||||
videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
||||
if strongSelf.imageScale != imageScale {
|
||||
|
@ -365,7 +365,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
|
||||
}
|
||||
self.avatarHeader = avatarHeader
|
||||
|
||||
var headers: [ListViewItemHeader] = [self.dateHeader]
|
||||
var headers: [ListViewItemHeader] = []
|
||||
if !self.disableDate {
|
||||
headers.append(self.dateHeader)
|
||||
}
|
||||
if case .messageOptions = associatedData.subject {
|
||||
headers = []
|
||||
}
|
||||
|
@ -223,7 +223,10 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch position {
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil {
|
||||
if incoming {
|
||||
@ -252,6 +255,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
var statusSize = CGSize()
|
||||
|
@ -284,7 +284,10 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch preparePosition {
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch preparePosition {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
statusType = .ImageIncoming
|
||||
@ -301,6 +304,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
statusType = nil
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
var isReplyThread = false
|
||||
|
@ -986,7 +986,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch position {
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
@ -1001,8 +1004,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
||||
|
||||
if let statusType = statusType {
|
||||
|
@ -77,21 +77,25 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
if message.flags.contains(.Failed) {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
} else if message.flags.isSending && !message.isSentOrAcknowledged {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
default:
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
statusType = nil
|
||||
} else {
|
||||
switch position {
|
||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
} else {
|
||||
if message.flags.contains(.Failed) {
|
||||
statusType = .BubbleOutgoing(.Failed)
|
||||
} else if message.flags.isSending && !message.isSentOrAcknowledged {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
default:
|
||||
statusType = nil
|
||||
}
|
||||
}
|
||||
|
||||
let entities = [MessageTextEntity(range: 0..<rawText.count, type: .Italic)]
|
||||
|
@ -458,8 +458,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
|
||||
if !isBroadcastChannel {
|
||||
hasAvatar = true
|
||||
} else if case .feed = item.chatLocation {
|
||||
hasAvatar = true
|
||||
} else if case .customChatContents = item.chatLocation {
|
||||
hasAvatar = false
|
||||
}
|
||||
}
|
||||
} else if incoming {
|
||||
@ -484,8 +484,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
} else if incoming {
|
||||
hasAvatar = true
|
||||
}
|
||||
case .feed:
|
||||
hasAvatar = true
|
||||
case .customChatContents:
|
||||
hasAvatar = false
|
||||
}
|
||||
|
||||
if hasAvatar {
|
||||
@ -975,6 +975,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
||||
dateAndStatusApply(animation)
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
strongSelf.dateAndStatusNode.isHidden = true
|
||||
}
|
||||
|
||||
if let updatedShareButtonNode = updatedShareButtonNode {
|
||||
if updatedShareButtonNode !== strongSelf.shareButtonNode {
|
||||
|
@ -263,6 +263,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if case .customChatContents = item.associatedData.subject {
|
||||
displayStatus = false
|
||||
}
|
||||
if displayStatus {
|
||||
if incoming {
|
||||
statusType = .BubbleIncoming
|
||||
|
@ -14,7 +14,8 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/SliderComponent,
|
||||
"//submodules/TelegramUI/Components/SliderComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -3,379 +3,141 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import LegacyUI
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import ListSectionComponent
|
||||
import SliderComponent
|
||||
|
||||
final class ListItemSliderSelectorComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
public final class ListItemSliderSelectorComponent: Component {
|
||||
public let theme: PresentationTheme
|
||||
public let values: [String]
|
||||
public let selectedIndex: Int
|
||||
public let selectedIndexUpdated: (Int) -> Void
|
||||
|
||||
let title: String
|
||||
let value: Float
|
||||
let minValue: Float
|
||||
let maxValue: Float
|
||||
let startValue: Float
|
||||
let isEnabled: Bool
|
||||
let trackColor: UIColor?
|
||||
let displayValue: Bool
|
||||
let valueUpdated: (Float) -> Void
|
||||
let isTrackingUpdated: ((Bool) -> Void)?
|
||||
|
||||
init(
|
||||
title: String,
|
||||
value: Float,
|
||||
minValue: Float,
|
||||
maxValue: Float,
|
||||
startValue: Float,
|
||||
isEnabled: Bool,
|
||||
trackColor: UIColor?,
|
||||
displayValue: Bool,
|
||||
valueUpdated: @escaping (Float) -> Void,
|
||||
isTrackingUpdated: ((Bool) -> Void)? = nil
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
values: [String],
|
||||
selectedIndex: Int,
|
||||
selectedIndexUpdated: @escaping (Int) -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.minValue = minValue
|
||||
self.maxValue = maxValue
|
||||
self.startValue = startValue
|
||||
self.isEnabled = isEnabled
|
||||
self.trackColor = trackColor
|
||||
self.displayValue = displayValue
|
||||
self.valueUpdated = valueUpdated
|
||||
self.isTrackingUpdated = isTrackingUpdated
|
||||
self.theme = theme
|
||||
self.values = values
|
||||
self.selectedIndex = selectedIndex
|
||||
self.selectedIndexUpdated = selectedIndexUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
public static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
if lhs.values != rhs.values {
|
||||
return false
|
||||
}
|
||||
if lhs.minValue != rhs.minValue {
|
||||
return false
|
||||
}
|
||||
if lhs.maxValue != rhs.maxValue {
|
||||
return false
|
||||
}
|
||||
if lhs.startValue != rhs.startValue {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.trackColor != rhs.trackColor {
|
||||
return false
|
||||
}
|
||||
if lhs.displayValue != rhs.displayValue {
|
||||
if lhs.selectedIndex != rhs.selectedIndex {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UITextFieldDelegate {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let value = ComponentView<Empty>()
|
||||
private var sliderView: TGPhotoEditorSliderView?
|
||||
public final class View: UIView, ListSectionComponent.ChildView {
|
||||
private var titles: [ComponentView<Empty>] = []
|
||||
private var slider = ComponentView<Empty>()
|
||||
|
||||
private var component: ListItemSliderSelectorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public private(set) var separatorInset: CGFloat = 0.0
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var internalIsTrackingUpdated: ((Bool) -> Void)?
|
||||
if let isTrackingUpdated = component.isTrackingUpdated {
|
||||
internalIsTrackingUpdated = { [weak self] isTracking in
|
||||
if let self {
|
||||
if isTracking {
|
||||
self.sliderView?.bordered = true
|
||||
} else {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.sliderView?.bordered = false
|
||||
}
|
||||
}
|
||||
isTrackingUpdated(isTracking)
|
||||
let transition: Transition
|
||||
if isTracking {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
if let titleView = self.title.view {
|
||||
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
if let valueView = self.value.view {
|
||||
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sliderView: TGPhotoEditorSliderView
|
||||
if let current = self.sliderView {
|
||||
sliderView = current
|
||||
sliderView.value = CGFloat(component.value)
|
||||
} else {
|
||||
sliderView = TGPhotoEditorSliderView()
|
||||
sliderView.backgroundColor = .clear
|
||||
sliderView.startColor = UIColor(rgb: 0xffffff)
|
||||
sliderView.enablePanHandling = true
|
||||
sliderView.trackCornerRadius = 1.0
|
||||
sliderView.lineSize = 2.0
|
||||
sliderView.minimumValue = CGFloat(component.minValue)
|
||||
sliderView.maximumValue = CGFloat(component.maxValue)
|
||||
sliderView.startValue = CGFloat(component.startValue)
|
||||
sliderView.value = CGFloat(component.value)
|
||||
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
|
||||
sliderView.layer.allowsGroupOpacity = true
|
||||
self.sliderView = sliderView
|
||||
self.addSubview(sliderView)
|
||||
}
|
||||
sliderView.interactionBegan = {
|
||||
internalIsTrackingUpdated?(true)
|
||||
}
|
||||
sliderView.interactionEnded = {
|
||||
internalIsTrackingUpdated?(false)
|
||||
}
|
||||
let sideInset: CGFloat = 13.0
|
||||
let titleSideInset: CGFloat = 20.0
|
||||
let titleClippingSideInset: CGFloat = 14.0
|
||||
|
||||
if component.isEnabled {
|
||||
sliderView.alpha = 1.3
|
||||
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
|
||||
sliderView.isUserInteractionEnabled = true
|
||||
} else {
|
||||
sliderView.trackColor = UIColor(rgb: 0xffffff)
|
||||
sliderView.alpha = 0.3
|
||||
sliderView.isUserInteractionEnabled = false
|
||||
}
|
||||
let titleAreaWidth: CGFloat = availableSize.width - titleSideInset * 2.0
|
||||
|
||||
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
|
||||
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
|
||||
}
|
||||
|
||||
let valueText: String
|
||||
if component.displayValue {
|
||||
if component.value > 0.005 {
|
||||
valueText = String(format: "+%.2f", component.value)
|
||||
} else if component.value < -0.005 {
|
||||
valueText = String(format: "%.2f", component.value)
|
||||
for i in 0 ..< component.values.count {
|
||||
var titleTransition = transition
|
||||
let title: ComponentView<Empty>
|
||||
if self.titles.count > i {
|
||||
title = self.titles[i]
|
||||
} else {
|
||||
valueText = ""
|
||||
titleTransition = titleTransition.withAnimation(.none)
|
||||
title = ComponentView()
|
||||
self.titles.append(title)
|
||||
}
|
||||
} else {
|
||||
valueText = ""
|
||||
}
|
||||
|
||||
let valueSize = self.value.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xf8d74a))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let valueView = self.value.view {
|
||||
if valueView.superview == nil {
|
||||
self.addSubview(valueView)
|
||||
}
|
||||
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: 52.0)
|
||||
}
|
||||
|
||||
@objc private func sliderValueChanged() {
|
||||
guard let component = self.component, let sliderView = self.sliderView else {
|
||||
return
|
||||
}
|
||||
component.valueUpdated(Float(sliderView.value))
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
struct AdjustmentTool: Equatable {
|
||||
let key: EditorToolKey
|
||||
let title: String
|
||||
let value: Float
|
||||
let minValue: Float
|
||||
let maxValue: Float
|
||||
let startValue: Float
|
||||
}
|
||||
|
||||
final class AdjustmentsComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let tools: [AdjustmentTool]
|
||||
let valueUpdated: (EditorToolKey, Float) -> Void
|
||||
let isTrackingUpdated: (Bool) -> Void
|
||||
|
||||
init(
|
||||
tools: [AdjustmentTool],
|
||||
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
|
||||
isTrackingUpdated: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.tools = tools
|
||||
self.valueUpdated = valueUpdated
|
||||
self.isTrackingUpdated = isTrackingUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
|
||||
if lhs.tools != rhs.tools {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let scrollView = UIScrollView()
|
||||
private var toolViews: [ComponentView<Empty>] = []
|
||||
|
||||
private var component: AdjustmentsComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let valueUpdated = component.valueUpdated
|
||||
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
|
||||
component.isTrackingUpdated(isTracking)
|
||||
|
||||
if let self {
|
||||
for i in 0 ..< component.tools.count {
|
||||
let tool = component.tools[i]
|
||||
if tool.key != trackingTool && i < self.toolViews.count {
|
||||
if let view = self.toolViews[i].view {
|
||||
let transition: Transition
|
||||
if isTracking {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sizes: [CGSize] = []
|
||||
for i in 0 ..< component.tools.count {
|
||||
let tool = component.tools[i]
|
||||
let componentView: ComponentView<Empty>
|
||||
if i >= self.toolViews.count {
|
||||
componentView = ComponentView<Empty>()
|
||||
self.toolViews.append(componentView)
|
||||
} else {
|
||||
componentView = self.toolViews[i]
|
||||
}
|
||||
|
||||
var valueIsNegative = false
|
||||
var value = tool.value
|
||||
if case .enhance = tool.key {
|
||||
if value < 0.0 {
|
||||
valueIsNegative = true
|
||||
}
|
||||
value = abs(value)
|
||||
}
|
||||
|
||||
let size = componentView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
ListItemSliderSelectorComponent(
|
||||
title: tool.title,
|
||||
value: value,
|
||||
minValue: tool.minValue,
|
||||
maxValue: tool.maxValue,
|
||||
startValue: tool.startValue,
|
||||
isEnabled: true,
|
||||
trackColor: nil,
|
||||
displayValue: true,
|
||||
valueUpdated: { value in
|
||||
var updatedValue = value
|
||||
if valueIsNegative {
|
||||
updatedValue *= -1.0
|
||||
}
|
||||
valueUpdated(tool.key, updatedValue)
|
||||
},
|
||||
isTrackingUpdated: { isTracking in
|
||||
isTrackingUpdated(tool.key, isTracking)
|
||||
}
|
||||
)
|
||||
),
|
||||
let titleSize = title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.values[i], font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
sizes.append(size)
|
||||
}
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
|
||||
for i in 0 ..< component.tools.count {
|
||||
let size = sizes[i]
|
||||
let componentView = self.toolViews[i]
|
||||
|
||||
if let view = componentView.view {
|
||||
if view.superview == nil {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
|
||||
var titleFrame = CGRect(origin: CGPoint(x: titleSideInset - floor(titleSize.width * 0.5), y: 14.0), size: titleSize)
|
||||
if component.values.count > 1 {
|
||||
titleFrame.origin.x += floor(CGFloat(i) / CGFloat(component.values.count - 1) * titleAreaWidth)
|
||||
}
|
||||
origin = origin.offsetBy(dx: 0.0, dy: size.height)
|
||||
if titleFrame.minX < titleClippingSideInset {
|
||||
titleFrame.origin.x = titleSideInset
|
||||
}
|
||||
if titleFrame.maxX > availableSize.width - titleClippingSideInset {
|
||||
titleFrame.origin.x = availableSize.width - titleClippingSideInset - titleSize.width
|
||||
}
|
||||
if let titleView = title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
titleTransition.setPosition(view: titleView, position: titleFrame.center)
|
||||
}
|
||||
}
|
||||
if self.titles.count > component.values.count {
|
||||
for i in component.values.count ..< self.titles.count {
|
||||
self.titles[i].view?.removeFromSuperview()
|
||||
}
|
||||
self.titles.removeLast(self.titles.count - component.values.count)
|
||||
}
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 180.0)
|
||||
let contentSize = CGSize(width: availableSize.width, height: origin.y)
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
let sliderSize = self.slider.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(SliderComponent(
|
||||
valueCount: component.values.count,
|
||||
value: component.selectedIndex,
|
||||
trackBackgroundColor: component.theme.list.controlSecondaryColor,
|
||||
trackForegroundColor: component.theme.list.itemAccentColor,
|
||||
valueUpdated: { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.selectedIndexUpdated(value)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
let sliderFrame = CGRect(origin: CGPoint(x: sideInset, y: 36.0), size: sliderSize)
|
||||
if let sliderView = self.slider.view {
|
||||
if sliderView.superview == nil {
|
||||
self.addSubview(sliderView)
|
||||
}
|
||||
transition.setFrame(view: sliderView, frame: sliderFrame)
|
||||
}
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
return size
|
||||
|
||||
self.separatorInset = 16.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: 88.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,78 +145,7 @@ final class AdjustmentsComponent: Component {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class AdjustmentsScreenComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let toggleUneditedPreview: (Bool) -> Void
|
||||
|
||||
init(
|
||||
toggleUneditedPreview: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.toggleUneditedPreview = toggleUneditedPreview
|
||||
}
|
||||
|
||||
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
enum Field {
|
||||
case blacks
|
||||
case shadows
|
||||
case midtones
|
||||
case highlights
|
||||
case whites
|
||||
}
|
||||
|
||||
private var component: AdjustmentsScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
|
||||
longPressGestureRecognizer.minimumPressDuration = 0.05
|
||||
self.addGestureRecognizer(longPressGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
component.toggleUneditedPreview(true)
|
||||
case .ended, .cancelled:
|
||||
component.toggleUneditedPreview(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,8 @@ swift_library(
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
"//submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen",
|
||||
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,6 +21,7 @@ import Markdown
|
||||
import LocationUI
|
||||
import TelegramStringFormatting
|
||||
import PlainButtonComponent
|
||||
import TimeSelectionActionSheet
|
||||
|
||||
final class BusinessDaySetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
@ -20,6 +20,7 @@ import LottieComponent
|
||||
import Markdown
|
||||
import LocationUI
|
||||
import TelegramStringFormatting
|
||||
import TimezoneSelectionScreen
|
||||
|
||||
final class BusinessHoursSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -75,7 +76,9 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let generalSection = ComponentView<Empty>()
|
||||
private let daysSection = ComponentView<Empty>()
|
||||
private let timezoneSection = ComponentView<Empty>()
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: BusinessHoursSetupScreenComponent?
|
||||
@ -84,6 +87,7 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
|
||||
private var showHours: Bool = false
|
||||
private var days: [Day] = []
|
||||
private var timezoneId: String
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
@ -98,6 +102,8 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
self.timezoneId = TimeZone.current.identifier
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
@ -126,7 +132,9 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
var scrolledUp = true
|
||||
@ -320,6 +328,8 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
contentHeight += generalSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
|
||||
var daysContentHeight: CGFloat = 0.0
|
||||
|
||||
var daysSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
for day in self.days {
|
||||
let dayIndex = daysSectionItems.count
|
||||
@ -441,7 +451,7 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: daysSectionSize)
|
||||
let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: daysSectionSize)
|
||||
if let daysSectionView = self.daysSection.view {
|
||||
if daysSectionView.superview == nil {
|
||||
daysSectionView.layer.allowsGroupOpacity = true
|
||||
@ -452,13 +462,80 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
|
||||
alphaTransition.setAlpha(view: daysSectionView, alpha: self.showHours ? 1.0 : 0.0)
|
||||
}
|
||||
daysContentHeight += daysSectionSize.height
|
||||
daysContentHeight += sectionSpacing
|
||||
|
||||
let timezoneSectionSize = self.timezoneSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Time Zone", //TODO:localize
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: TimeZone(identifier: self.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? self.timezoneId,
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)))),
|
||||
accessory: .arrow,
|
||||
action: { [weak self] _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
var completed: ((String) -> Void)?
|
||||
let controller = TimezoneSelectionScreen(context: component.context, completed: { timezoneId in
|
||||
completed?(timezoneId)
|
||||
})
|
||||
controller.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(controller)
|
||||
completed = { [weak self, weak controller] timezoneId in
|
||||
guard let self else {
|
||||
controller?.dismiss()
|
||||
return
|
||||
}
|
||||
self.timezoneId = timezoneId
|
||||
self.state?.updated(transition: .immediate)
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
)))
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let timezoneSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: timezoneSectionSize)
|
||||
if let timezoneSectionView = self.timezoneSection.view {
|
||||
if timezoneSectionView.superview == nil {
|
||||
self.scrollView.addSubview(timezoneSectionView)
|
||||
}
|
||||
transition.setFrame(view: timezoneSectionView, frame: timezoneSectionFrame)
|
||||
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
|
||||
alphaTransition.setAlpha(view: timezoneSectionView, alpha: self.showHours ? 1.0 : 0.0)
|
||||
}
|
||||
daysContentHeight += timezoneSectionSize.height
|
||||
|
||||
if self.showHours {
|
||||
contentHeight += daysSectionSize.height
|
||||
contentHeight += daysContentHeight
|
||||
}
|
||||
|
||||
contentHeight += bottomContentInset
|
||||
contentHeight += environment.safeInsets.bottom
|
||||
|
||||
self.ignoreScrolling = true
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
@ -472,6 +549,7 @@ final class BusinessHoursSetupScreenComponent: Component {
|
||||
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
|
@ -263,7 +263,11 @@ final class BusinessSetupScreenComponent: Component {
|
||||
icon: "Settings/Menu/Photos",
|
||||
title: "Quick Replies",
|
||||
subtitle: "Set up shortcuts with rich text and media to respond to messages faster.",
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(component.context.sharedContext.makeQuickReplySetupScreen(context: component.context))
|
||||
}
|
||||
))
|
||||
items.append(Item(
|
||||
@ -274,14 +278,18 @@ final class BusinessSetupScreenComponent: Component {
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context))
|
||||
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context, isAwayMode: false))
|
||||
}
|
||||
))
|
||||
items.append(Item(
|
||||
icon: "Settings/Menu/Trending",
|
||||
title: "Away Messages",
|
||||
subtitle: "Define messages that are automatically sent when you are off.",
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context, isAwayMode: true))
|
||||
}
|
||||
))
|
||||
items.append(Item(
|
||||
|
@ -34,7 +34,17 @@ swift_library(
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
|
||||
"//submodules/ShimmerEffect",
|
||||
"//submodules/ChatListUI",
|
||||
"//submodules/MergeLists",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/ItemListPeerActionItem",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
|
||||
"//submodules/DateSelectionUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,314 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ListSectionComponent
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import ChatListUI
|
||||
import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class GreetingMessageListItemComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let accountPeer: EnginePeer
|
||||
let message: EngineMessage
|
||||
let count: Int
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
accountPeer: EnginePeer,
|
||||
message: EngineMessage,
|
||||
count: Int,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.accountPeer = accountPeer
|
||||
self.message = message
|
||||
self.count = count
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: GreetingMessageListItemComponent, rhs: GreetingMessageListItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.accountPeer != rhs.accountPeer {
|
||||
return false
|
||||
}
|
||||
if lhs.message != rhs.message {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
if (lhs.action == nil) != (rhs.action == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: HighlightTrackingButton, ListSectionComponent.ChildView {
|
||||
private var component: GreetingMessageListItemComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
private var chatListPresentationData: ChatListPresentationData?
|
||||
private var chatListNodeInteraction: ChatListNodeInteraction?
|
||||
|
||||
private var itemNode: ListViewItemNode?
|
||||
|
||||
var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
private(set) var separatorInset: CGFloat = 0.0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.internalHighligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self, let component = self.component, component.action != nil else {
|
||||
return
|
||||
}
|
||||
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
|
||||
customUpdateIsHighlighted(isHighlighted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.component?.action?()
|
||||
}
|
||||
|
||||
func update(component: GreetingMessageListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
self.isEnabled = component.action != nil
|
||||
|
||||
let chatListPresentationData: ChatListPresentationData
|
||||
if let current = self.chatListPresentationData, let previousComponent, previousComponent.theme === component.theme {
|
||||
chatListPresentationData = current
|
||||
} else {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
chatListPresentationData = ChatListPresentationData(
|
||||
theme: component.theme,
|
||||
fontSize: presentationData.listsFontSize,
|
||||
strings: component.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameSortOrder: presentationData.nameSortOrder,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
disableAnimations: false
|
||||
)
|
||||
self.chatListPresentationData = chatListPresentationData
|
||||
}
|
||||
|
||||
let chatListNodeInteraction: ChatListNodeInteraction
|
||||
if let current = self.chatListNodeInteraction {
|
||||
chatListNodeInteraction = current
|
||||
} else {
|
||||
chatListNodeInteraction = ChatListNodeInteraction(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
activateSearch: {
|
||||
},
|
||||
peerSelected: { _, _, _, _ in
|
||||
},
|
||||
disabledPeerSelected: { _, _, _ in
|
||||
},
|
||||
togglePeerSelected: { _, _ in
|
||||
},
|
||||
togglePeersSelection: { _, _ in
|
||||
},
|
||||
additionalCategorySelected: { _ in
|
||||
},
|
||||
messageSelected: { _, _, _, _ in
|
||||
},
|
||||
groupSelected: { _ in
|
||||
},
|
||||
addContact: { _ in
|
||||
},
|
||||
setPeerIdWithRevealedOptions: { _, _ in
|
||||
},
|
||||
setItemPinned: { _, _ in
|
||||
},
|
||||
setPeerMuted: { _, _ in
|
||||
},
|
||||
setPeerThreadMuted: { _, _, _ in
|
||||
},
|
||||
deletePeer: { _, _ in
|
||||
},
|
||||
deletePeerThread: { _, _ in
|
||||
},
|
||||
setPeerThreadStopped: { _, _, _ in
|
||||
},
|
||||
setPeerThreadPinned: { _, _, _ in
|
||||
},
|
||||
setPeerThreadHidden: { _, _, _ in
|
||||
},
|
||||
updatePeerGrouping: { _, _ in
|
||||
},
|
||||
togglePeerMarkedUnread: { _, _ in
|
||||
},
|
||||
toggleArchivedFolderHiddenByDefault: {
|
||||
},
|
||||
toggleThreadsSelection: { _, _ in
|
||||
},
|
||||
hidePsa: { _ in
|
||||
},
|
||||
activateChatPreview: { _, _, _, _, _ in
|
||||
},
|
||||
present: { _ in
|
||||
},
|
||||
openForumThread: { _, _ in
|
||||
},
|
||||
openStorageManagement: {
|
||||
},
|
||||
openPasswordSetup: {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: {
|
||||
},
|
||||
openActiveSessions: {
|
||||
},
|
||||
performActiveSessionAction: { _, _ in
|
||||
},
|
||||
openChatFolderUpdates: {
|
||||
},
|
||||
hideChatFolderUpdates: {
|
||||
},
|
||||
openStories: { _, _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
}
|
||||
)
|
||||
self.chatListNodeInteraction = chatListNodeInteraction
|
||||
}
|
||||
|
||||
let chatListItem = ChatListItem(
|
||||
presentationData: chatListPresentationData,
|
||||
context: component.context,
|
||||
chatListLocation: .chatList(groupId: .root),
|
||||
filterData: nil,
|
||||
index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: component.message.index)),
|
||||
content: .peer(ChatListItemContent.PeerData(
|
||||
messages: [component.message],
|
||||
peer: EngineRenderedPeer(peer: component.accountPeer),
|
||||
threadInfo: nil,
|
||||
combinedReadState: nil,
|
||||
isRemovedFromTotalUnreadCount: false,
|
||||
presence: nil,
|
||||
hasUnseenMentions: false,
|
||||
hasUnseenReactions: false,
|
||||
draftState: nil,
|
||||
mediaDraftContentType: nil,
|
||||
inputActivities: nil,
|
||||
promoInfo: nil,
|
||||
ignoreUnreadBadge: false,
|
||||
displayAsMessage: false,
|
||||
hasFailedMessages: false,
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
storyState: nil,
|
||||
requiresPremiumForMessaging: false,
|
||||
displayAsTopicList: false,
|
||||
tags: [],
|
||||
customMessageListData: ChatListItemContent.CustomMessageListData(
|
||||
commandPrefix: nil,
|
||||
messageCount: component.count
|
||||
)
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
selected: false,
|
||||
header: nil,
|
||||
enableContextActions: false,
|
||||
hiddenOffset: false,
|
||||
interaction: chatListNodeInteraction
|
||||
)
|
||||
var itemNode: ListViewItemNode?
|
||||
let params = ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0)
|
||||
if let current = self.itemNode {
|
||||
itemNode = current
|
||||
chatListItem.updateNode(
|
||||
async: { f in f () },
|
||||
node: {
|
||||
return current
|
||||
},
|
||||
params: params,
|
||||
previousItem: nil,
|
||||
nextItem: nil, animation: .None,
|
||||
completion: { layout, apply in
|
||||
let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||
|
||||
current.contentSize = layout.contentSize
|
||||
current.insets = layout.insets
|
||||
current.frame = nodeFrame
|
||||
|
||||
apply(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
} else {
|
||||
var outItemNode: ListViewItemNode?
|
||||
chatListItem.nodeConfiguredForParams(
|
||||
async: { f in f() },
|
||||
params: params,
|
||||
synchronousLoads: true,
|
||||
previousItem: nil,
|
||||
nextItem: nil,
|
||||
completion: { node, apply in
|
||||
outItemNode = node
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
}
|
||||
)
|
||||
itemNode = outItemNode
|
||||
}
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: itemNode?.contentSize.height ?? 44.0)
|
||||
|
||||
if self.itemNode !== itemNode {
|
||||
self.itemNode?.removeFromSupernode()
|
||||
|
||||
self.itemNode = itemNode
|
||||
if let itemNode {
|
||||
itemNode.isUserInteractionEnabled = false
|
||||
self.addSubview(itemNode.view)
|
||||
}
|
||||
}
|
||||
if let itemNode = self.itemNode {
|
||||
itemNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
|
||||
self.separatorInset = 76.0
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
final class GreetingMessageSetupChatContents: ChatCustomContentsProtocol {
|
||||
private final class Impl {
|
||||
let queue: Queue
|
||||
let context: AccountContext
|
||||
|
||||
private var messages: [Message] = []
|
||||
private var nextMessageId: Int32 = 1000
|
||||
let messagesPromise = Promise<[Message]>([])
|
||||
|
||||
private var nextGroupingId: UInt32 = 0
|
||||
private var groupingKeyToGroupId: [Int64: UInt32] = [:]
|
||||
|
||||
init(queue: Queue, context: AccountContext, messages: [EngineMessage]) {
|
||||
self.queue = queue
|
||||
self.context = context
|
||||
self.messages = messages.map { $0._asMessage() }
|
||||
self.notifyMessagesUpdated()
|
||||
|
||||
if let maxMessageId = messages.map(\.id).max() {
|
||||
self.nextMessageId = maxMessageId.id + 1
|
||||
}
|
||||
if let maxGroupingId = messages.compactMap(\.groupInfo?.stableId).max() {
|
||||
self.nextGroupingId = maxGroupingId + 1
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
private func notifyMessagesUpdated() {
|
||||
self.messages.sort(by: { $0.index > $1.index })
|
||||
self.messagesPromise.set(.single(self.messages))
|
||||
}
|
||||
|
||||
func enqueueMessages(messages: [EnqueueMessage]) {
|
||||
for message in messages {
|
||||
switch message {
|
||||
case let .message(text, attributes, _, mediaReference, _, _, _, localGroupingKey, correlationId, _):
|
||||
let _ = attributes
|
||||
let _ = mediaReference
|
||||
let _ = correlationId
|
||||
|
||||
let messageId = self.nextMessageId
|
||||
self.nextMessageId += 1
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
attributes.append(OutgoingMessageInfoAttribute(
|
||||
uniqueId: Int64.random(in: Int64.min ... Int64.max),
|
||||
flags: [],
|
||||
acknowledged: true,
|
||||
correlationId: correlationId,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
))
|
||||
|
||||
var media: [Media] = []
|
||||
if let mediaReference {
|
||||
media.append(mediaReference.media)
|
||||
}
|
||||
|
||||
let mappedMessage = Message(
|
||||
stableId: UInt32(messageId),
|
||||
stableVersion: 0,
|
||||
id: MessageId(
|
||||
peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)),
|
||||
namespace: Namespaces.Message.Local,
|
||||
id: Int32(messageId)
|
||||
),
|
||||
globallyUniqueId: nil,
|
||||
groupingKey: localGroupingKey,
|
||||
groupInfo: localGroupingKey.flatMap { value in
|
||||
if let current = self.groupingKeyToGroupId[value] {
|
||||
return MessageGroupInfo(stableId: current)
|
||||
} else {
|
||||
let groupId = self.nextGroupingId
|
||||
self.nextGroupingId += 1
|
||||
self.groupingKeyToGroupId[value] = groupId
|
||||
return MessageGroupInfo(stableId: groupId)
|
||||
}
|
||||
},
|
||||
threadId: nil,
|
||||
timestamp: messageId,
|
||||
flags: [],
|
||||
tags: [],
|
||||
globalTags: [],
|
||||
localTags: [],
|
||||
customTags: [],
|
||||
forwardInfo: nil,
|
||||
author: nil,
|
||||
text: text,
|
||||
attributes: attributes,
|
||||
media: media,
|
||||
peers: SimpleDictionary(),
|
||||
associatedMessages: SimpleDictionary(),
|
||||
associatedMessageIds: [],
|
||||
associatedMedia: [:],
|
||||
associatedThreadInfo: nil,
|
||||
associatedStories: [:]
|
||||
)
|
||||
self.messages.append(mappedMessage)
|
||||
case .forward:
|
||||
break
|
||||
}
|
||||
}
|
||||
self.notifyMessagesUpdated()
|
||||
}
|
||||
|
||||
func deleteMessages(ids: [EngineMessage.Id]) {
|
||||
self.messages = self.messages.filter({ !ids.contains($0.id) })
|
||||
self.notifyMessagesUpdated()
|
||||
}
|
||||
|
||||
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
|
||||
guard let index = self.messages.firstIndex(where: { $0.id == id }) else {
|
||||
return
|
||||
}
|
||||
let originalMessage = self.messages[index]
|
||||
|
||||
var mappedMedia = originalMessage.media
|
||||
switch media {
|
||||
case .keep:
|
||||
break
|
||||
case let .update(value):
|
||||
mappedMedia = [value.media]
|
||||
}
|
||||
|
||||
var mappedAtrributes = originalMessage.attributes
|
||||
mappedAtrributes.removeAll(where: { $0 is TextEntitiesMessageAttribute })
|
||||
if let entities {
|
||||
mappedAtrributes.append(entities)
|
||||
}
|
||||
|
||||
let mappedMessage = Message(
|
||||
stableId: originalMessage.stableId,
|
||||
stableVersion: originalMessage.stableVersion + 1,
|
||||
id: originalMessage.id,
|
||||
globallyUniqueId: originalMessage.globallyUniqueId,
|
||||
groupingKey: originalMessage.groupingKey,
|
||||
groupInfo: originalMessage.groupInfo,
|
||||
threadId: originalMessage.threadId,
|
||||
timestamp: originalMessage.timestamp,
|
||||
flags: originalMessage.flags,
|
||||
tags: originalMessage.tags,
|
||||
globalTags: originalMessage.globalTags,
|
||||
localTags: originalMessage.localTags,
|
||||
customTags: originalMessage.customTags,
|
||||
forwardInfo: originalMessage.forwardInfo,
|
||||
author: originalMessage.author,
|
||||
text: text,
|
||||
attributes: mappedAtrributes,
|
||||
media: mappedMedia,
|
||||
peers: originalMessage.peers,
|
||||
associatedMessages: originalMessage.associatedMessages,
|
||||
associatedMessageIds: originalMessage.associatedMessageIds,
|
||||
associatedMedia: originalMessage.associatedMedia,
|
||||
associatedThreadInfo: originalMessage.associatedThreadInfo,
|
||||
associatedStories: originalMessage.associatedStories
|
||||
)
|
||||
|
||||
self.messages[index] = mappedMessage
|
||||
self.notifyMessagesUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
let kind: ChatCustomContentsKind
|
||||
|
||||
var messages: Signal<[Message], NoError> {
|
||||
return self.impl.signalWith({ impl, subscriber in
|
||||
return impl.messagesPromise.get().start(next: subscriber.putNext)
|
||||
})
|
||||
}
|
||||
|
||||
var messageLimit: Int? {
|
||||
return 20
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
init(context: AccountContext, messages: [EngineMessage], kind: ChatCustomContentsKind) {
|
||||
self.kind = kind
|
||||
|
||||
let queue = Queue()
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, context: context, messages: messages)
|
||||
})
|
||||
}
|
||||
|
||||
func enqueueMessages(messages: [EnqueueMessage]) {
|
||||
self.impl.with { impl in
|
||||
impl.enqueueMessages(messages: messages)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteMessages(ids: [EngineMessage.Id]) {
|
||||
self.impl.with { impl in
|
||||
impl.deleteMessages(ids: ids)
|
||||
}
|
||||
}
|
||||
|
||||
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
|
||||
self.impl.with { impl in
|
||||
impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,11 @@ import LottieComponent
|
||||
import Markdown
|
||||
import PeerListItemComponent
|
||||
import AvatarNode
|
||||
import ListItemSliderSelectorComponent
|
||||
import DateSelectionUI
|
||||
import PlainButtonComponent
|
||||
import TelegramStringFormatting
|
||||
import TimeSelectionActionSheet
|
||||
|
||||
private let checkIcon: UIImage = {
|
||||
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
|
||||
@ -41,17 +46,23 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let mode: GreetingMessageSetupScreen.Mode
|
||||
|
||||
init(
|
||||
context: AccountContext
|
||||
context: AccountContext,
|
||||
mode: GreetingMessageSetupScreen.Mode
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
static func ==(lhs: GreetingMessageSetupScreenComponent, rhs: GreetingMessageSetupScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.mode != rhs.mode {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -62,22 +73,6 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private struct BotResolutionState: Equatable {
|
||||
enum State: Equatable {
|
||||
case searching
|
||||
case notFound
|
||||
case found(peer: EnginePeer, isInstalled: Bool)
|
||||
}
|
||||
|
||||
var query: String
|
||||
var state: State
|
||||
|
||||
init(query: String, state: State) {
|
||||
self.query = query
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
private struct AdditionalPeerList {
|
||||
enum Category: Int {
|
||||
case newChats = 0
|
||||
@ -105,6 +100,12 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private enum Schedule {
|
||||
case always
|
||||
case outsideBusinessHours
|
||||
case custom
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
@ -113,19 +114,27 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let generalSection = ComponentView<Empty>()
|
||||
private let messagesSection = ComponentView<Empty>()
|
||||
private let scheduleSection = ComponentView<Empty>()
|
||||
private let customScheduleSection = ComponentView<Empty>()
|
||||
private let accessSection = ComponentView<Empty>()
|
||||
private let excludedSection = ComponentView<Empty>()
|
||||
private let permissionsSection = ComponentView<Empty>()
|
||||
private let periodSection = ComponentView<Empty>()
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: GreetingMessageSetupScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var chevronImage: UIImage?
|
||||
|
||||
private var isOn: Bool = false
|
||||
private var accountPeer: EnginePeer?
|
||||
private var messages: [EngineMessage] = []
|
||||
|
||||
private var schedule: Schedule = .always
|
||||
private var customScheduleStart: Date?
|
||||
private var customScheduleEnd: Date?
|
||||
|
||||
private var hasAccessToAllChatsByDefault: Bool = true
|
||||
private var additionalPeerList = AdditionalPeerList(
|
||||
@ -135,6 +144,8 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
|
||||
private var replyToMessages: Bool = true
|
||||
|
||||
private var messagesDisposable: Disposable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
@ -161,6 +172,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.messagesDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -172,7 +184,9 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
var scrolledUp = true
|
||||
@ -332,12 +346,140 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
self.environment?.controller()?.push(controller)
|
||||
}
|
||||
|
||||
private func openMessageList() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let contents = GreetingMessageSetupChatContents(
|
||||
context: component.context,
|
||||
messages: self.messages,
|
||||
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput
|
||||
)
|
||||
let chatController = component.context.sharedContext.makeChatController(
|
||||
context: component.context,
|
||||
chatLocation: .customChatContents,
|
||||
subject: .customChatContents(contents: contents),
|
||||
botStart: nil,
|
||||
mode: .standard(.default)
|
||||
)
|
||||
chatController.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(chatController)
|
||||
self.messagesDisposable?.dispose()
|
||||
self.messagesDisposable = (contents.messages
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let messages = messages.map(EngineMessage.init)
|
||||
if self.messages != messages {
|
||||
self.messages = messages
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentValue: Date = (isStartTime ? self.customScheduleStart : self.customScheduleEnd) ?? Date()
|
||||
|
||||
if isDate {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.year, .month, .day], from: currentValue)
|
||||
guard let clippedDate = calendar.date(from: components) else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = DateSelectionActionSheetController(
|
||||
context: component.context,
|
||||
title: nil,
|
||||
currentValue: Int32(clippedDate.timeIntervalSince1970),
|
||||
minimumDate: nil,
|
||||
maximumDate: nil,
|
||||
emptyTitle: nil,
|
||||
applyValue: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let value else {
|
||||
return
|
||||
}
|
||||
let updatedDate = Date(timeIntervalSince1970: Double(value))
|
||||
let calendar = Calendar.current
|
||||
var updatedComponents = calendar.dateComponents([.year, .month, .day], from: updatedDate)
|
||||
let currentComponents = calendar.dateComponents([.hour, .minute], from: currentValue)
|
||||
updatedComponents.hour = currentComponents.hour
|
||||
updatedComponents.minute = currentComponents.minute
|
||||
guard let updatedClippedDate = calendar.date(from: updatedComponents) else {
|
||||
return
|
||||
}
|
||||
|
||||
if isStartTime {
|
||||
self.customScheduleStart = updatedClippedDate
|
||||
} else {
|
||||
self.customScheduleEnd = updatedClippedDate
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
)
|
||||
self.environment?.controller()?.present(controller, in: .window(.root))
|
||||
} else {
|
||||
let calendar = Calendar.current
|
||||
let components = calendar.dateComponents([.hour, .minute], from: currentValue)
|
||||
let hour = components.hour ?? 0
|
||||
let minute = components.minute ?? 0
|
||||
|
||||
let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(hour * 60 * 60 + minute * 60), applyValue: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let value else {
|
||||
return
|
||||
}
|
||||
|
||||
let updatedHour = value / (60 * 60)
|
||||
let updatedMinute = (value % (60 * 60)) / 60
|
||||
|
||||
let calendar = Calendar.current
|
||||
var updatedComponents = calendar.dateComponents([.year, .month, .day], from: currentValue)
|
||||
updatedComponents.hour = Int(updatedHour)
|
||||
updatedComponents.minute = Int(updatedMinute)
|
||||
|
||||
guard let updatedClippedDate = calendar.date(from: updatedComponents) else {
|
||||
return
|
||||
}
|
||||
|
||||
if isStartTime {
|
||||
self.customScheduleStart = updatedClippedDate
|
||||
} else {
|
||||
self.customScheduleEnd = updatedClippedDate
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
self.environment?.controller()?.present(controller, in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: GreetingMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
let _ = (component.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.accountPeer = peer
|
||||
})
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
@ -357,7 +499,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Greeting Message", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: component.mode == .greeting ? "Greeting Message" : "Away Message", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
@ -387,7 +529,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "HandWaveEmoji"),
|
||||
content: LottieComponent.AppBundleContent(name: component.mode == .greeting ? "HandWaveEmoji" : "ZzzEmoji"),
|
||||
loop: true
|
||||
)),
|
||||
environment: {},
|
||||
@ -405,7 +547,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
contentHeight += 129.0
|
||||
|
||||
//TODO:localize
|
||||
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Greet customers when they message you the first time or after a period of no activity.", attributes: MarkdownAttributes(
|
||||
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(component.mode == .greeting ? "Greet customers when they message you the first time or after a period of no activity." : "Automatically reply with a message when you are away.", attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
|
||||
@ -413,12 +555,6 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
return ("URL", "")
|
||||
}), textAlignment: .center
|
||||
))
|
||||
if self.chevronImage == nil {
|
||||
self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight")
|
||||
}
|
||||
if let range = subtitleString.string.range(of: ">"), let chevronImage = self.chevronImage {
|
||||
subtitleString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: subtitleString.string))
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let subtitleSize = self.subtitle.update(
|
||||
@ -463,7 +599,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Send Greeting Message",
|
||||
string: component.mode == .greeting ? "Send Greeting Message" : "Send Away Message",
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
@ -504,6 +640,279 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
|
||||
var otherSectionsHeight: CGFloat = 0.0
|
||||
|
||||
//TODO:localize
|
||||
var messagesSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
if let topMessage = self.messages.first {
|
||||
if let accountPeer = self.accountPeer {
|
||||
messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
accountPeer: accountPeer,
|
||||
message: topMessage,
|
||||
count: self.messages.count,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openMessageList()
|
||||
}
|
||||
))))
|
||||
}
|
||||
} else {
|
||||
messagesSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.mode == .greeting ? "Create a Greeting Message" : "Create an Away Message",
|
||||
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/ComposeIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openMessageList()
|
||||
}
|
||||
))))
|
||||
}
|
||||
let messagesSectionSize = self.messagesSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.mode == .greeting ? (self.messages.count > 1 ? "GREETING MESSAGES" : "GREETING MESSAGE") : (self.messages.count > 1 ? "AWAY MESSAGES" : "AWAY MESSAGE"),
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
items: messagesSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let messagesSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: messagesSectionSize)
|
||||
if let messagesSectionView = self.messagesSection.view {
|
||||
if messagesSectionView.superview == nil {
|
||||
messagesSectionView.layer.allowsGroupOpacity = true
|
||||
self.scrollView.addSubview(messagesSectionView)
|
||||
}
|
||||
transition.setFrame(view: messagesSectionView, frame: messagesSectionFrame)
|
||||
alphaTransition.setAlpha(view: messagesSectionView, alpha: self.isOn ? 1.0 : 0.0)
|
||||
}
|
||||
otherSectionsHeight += messagesSectionSize.height
|
||||
otherSectionsHeight += sectionSpacing
|
||||
|
||||
if case .away = component.mode {
|
||||
//TODO:localize
|
||||
var scheduleSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
for i in 0 ..< 3 {
|
||||
let title: String
|
||||
let schedule: Schedule
|
||||
switch i {
|
||||
case 0:
|
||||
title = "Always Send"
|
||||
schedule = .always
|
||||
case 1:
|
||||
title = "Outside of Business Hours"
|
||||
schedule = .outsideBusinessHours
|
||||
default:
|
||||
title = "Custom Schedule"
|
||||
schedule = .custom
|
||||
}
|
||||
let isSelected = self.schedule == schedule
|
||||
scheduleSectionItems.append(AnyComponentWithIdentity(id: scheduleSectionItems.count, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: title,
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.schedule != schedule {
|
||||
self.schedule = schedule
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
))))
|
||||
}
|
||||
let scheduleSectionSize = self.scheduleSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "SCHEDULE",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
items: scheduleSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let scheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: scheduleSectionSize)
|
||||
if let scheduleSectionView = self.scheduleSection.view {
|
||||
if scheduleSectionView.superview == nil {
|
||||
scheduleSectionView.layer.allowsGroupOpacity = true
|
||||
self.scrollView.addSubview(scheduleSectionView)
|
||||
}
|
||||
transition.setFrame(view: scheduleSectionView, frame: scheduleSectionFrame)
|
||||
alphaTransition.setAlpha(view: scheduleSectionView, alpha: self.isOn ? 1.0 : 0.0)
|
||||
}
|
||||
otherSectionsHeight += scheduleSectionSize.height
|
||||
otherSectionsHeight += sectionSpacing
|
||||
|
||||
var customScheduleSectionsHeight: CGFloat = 0.0
|
||||
var customScheduleSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
for i in 0 ..< 2 {
|
||||
let title: String
|
||||
let itemDate: Date?
|
||||
let isStartTime: Bool
|
||||
switch i {
|
||||
case 0:
|
||||
title = "Start Time"
|
||||
itemDate = self.customScheduleStart
|
||||
isStartTime = true
|
||||
default:
|
||||
title = "End Time"
|
||||
itemDate = self.customScheduleEnd
|
||||
isStartTime = false
|
||||
}
|
||||
|
||||
var icon: ListActionItemComponent.Icon?
|
||||
if let itemDate {
|
||||
let calendar = Calendar.current
|
||||
let hours = calendar.component(.hour, from: itemDate)
|
||||
let minutes = calendar.component(.minute, from: itemDate)
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
let timeText = stringForShortTimestamp(hours: Int32(hours), minutes: Int32(minutes), dateTimeFormat: presentationData.dateTimeFormat)
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.dateStyle = .medium
|
||||
let dateText = stringForCompactDate(timestamp: Int32(itemDate.timeIntervalSince1970), strings: environment.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
|
||||
icon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: dateText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
|
||||
effectAlignment: .center,
|
||||
minSize: nil,
|
||||
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true)
|
||||
},
|
||||
animateAlpha: true,
|
||||
animateScale: false
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: timeText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
|
||||
effectAlignment: .center,
|
||||
minSize: nil,
|
||||
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: false)
|
||||
},
|
||||
animateAlpha: true,
|
||||
animateScale: false
|
||||
)))
|
||||
], spacing: 4.0))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true)
|
||||
}
|
||||
|
||||
customScheduleSectionItems.append(AnyComponentWithIdentity(id: customScheduleSectionItems.count, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: title,
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
icon: icon,
|
||||
accessory: nil,
|
||||
action: itemDate != nil ? nil : { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true)
|
||||
}
|
||||
))))
|
||||
}
|
||||
let customScheduleSectionSize = self.customScheduleSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: customScheduleSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let customScheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight + customScheduleSectionsHeight), size: customScheduleSectionSize)
|
||||
if let customScheduleSectionView = self.customScheduleSection.view {
|
||||
if customScheduleSectionView.superview == nil {
|
||||
customScheduleSectionView.layer.allowsGroupOpacity = true
|
||||
self.scrollView.addSubview(customScheduleSectionView)
|
||||
}
|
||||
transition.setFrame(view: customScheduleSectionView, frame: customScheduleSectionFrame)
|
||||
alphaTransition.setAlpha(view: customScheduleSectionView, alpha: (self.isOn && self.schedule == .custom) ? 1.0 : 0.0)
|
||||
}
|
||||
customScheduleSectionsHeight += customScheduleSectionSize.height
|
||||
customScheduleSectionsHeight += sectionSpacing
|
||||
|
||||
if self.schedule == .custom {
|
||||
otherSectionsHeight += customScheduleSectionsHeight
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let accessSectionSize = self.accessSection.update(
|
||||
transition: transition,
|
||||
@ -700,7 +1109,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
)),
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(
|
||||
text: self.hasAccessToAllChatsByDefault ? "Select chats or entire chat categories which the bot **WILL NOT** have access to." : "Select chats or entire chat categories which the bot **WILL** have access to.",
|
||||
text: component.mode == .greeting ? "Choose chats or entire chat categories for sending a greeting message." : "Choose chats or entire chat categories for sending an away message.",
|
||||
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),
|
||||
@ -729,65 +1138,61 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
otherSectionsHeight += excludedSectionSize.height
|
||||
otherSectionsHeight += sectionSpacing
|
||||
|
||||
//TODO:localize
|
||||
/*let permissionsSectionSize = self.permissionsSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "BOT PERMISSIONS",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
if case .greeting = component.mode {
|
||||
let periodSectionSize = self.periodSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "PERIOD OF NO ACTIVITY",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot.",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Choose how many days should pass after your last interaction with a recipient to send them the greeting in response to their message.",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Reply to Messages",
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
|
||||
theme: environment.theme,
|
||||
values: [
|
||||
"7 days",
|
||||
"14 days",
|
||||
"21 days",
|
||||
"28 days"
|
||||
],
|
||||
selectedIndex: 0,
|
||||
selectedIndexUpdated: { [weak self] index in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}
|
||||
self.replyToMessages = !self.replyToMessages
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
})),
|
||||
action: nil
|
||||
)))
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let permissionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: permissionsSectionSize)
|
||||
if let permissionsSectionView = self.permissionsSection.view {
|
||||
if permissionsSectionView.superview == nil {
|
||||
permissionsSectionView.layer.allowsGroupOpacity = true
|
||||
self.scrollView.addSubview(permissionsSectionView)
|
||||
)))
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let periodSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: periodSectionSize)
|
||||
if let periodSectionView = self.periodSection.view {
|
||||
if periodSectionView.superview == nil {
|
||||
periodSectionView.layer.allowsGroupOpacity = true
|
||||
self.scrollView.addSubview(periodSectionView)
|
||||
}
|
||||
transition.setFrame(view: periodSectionView, frame: periodSectionFrame)
|
||||
alphaTransition.setAlpha(view: periodSectionView, alpha: self.isOn ? 1.0 : 0.0)
|
||||
}
|
||||
transition.setFrame(view: permissionsSectionView, frame: permissionsSectionFrame)
|
||||
|
||||
alphaTransition.setAlpha(view: permissionsSectionView, alpha: self.isOn ? 1.0 : 0.0)
|
||||
otherSectionsHeight += periodSectionSize.height
|
||||
otherSectionsHeight += sectionSpacing
|
||||
}
|
||||
otherSectionsHeight += permissionsSectionSize.height*/
|
||||
|
||||
if self.isOn {
|
||||
contentHeight += otherSectionsHeight
|
||||
@ -799,6 +1204,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
self.ignoreScrolling = true
|
||||
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
||||
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
@ -809,6 +1215,7 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
@ -836,13 +1243,19 @@ final class GreetingMessageSetupScreenComponent: Component {
|
||||
}
|
||||
|
||||
public final class GreetingMessageSetupScreen: ViewControllerComponentContainer {
|
||||
public enum Mode {
|
||||
case greeting
|
||||
case away
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext) {
|
||||
public init(context: AccountContext, mode: Mode) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: GreetingMessageSetupScreenComponent(
|
||||
context: context
|
||||
context: context,
|
||||
mode: mode
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
@ -0,0 +1,181 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import ButtonComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
|
||||
final class QuickReplyEmptyStateComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let insets: UIEdgeInsets
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
insets: UIEdgeInsets,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.insets = insets
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: QuickReplyEmptyStateComponent, rhs: QuickReplyEmptyStateComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var component: QuickReplyEmptyStateComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: QuickReplyEmptyStateComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let _ = previousComponent
|
||||
|
||||
let iconTitleSpacing: CGFloat = 10.0
|
||||
let titleTextSpacing: CGFloat = 8.0
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "📝", font: Font.semibold(90.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
|
||||
//TODO:localize
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "No Quick Replies", font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Set up shortcuts with rich text and media to respond to messages faster.", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 20
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing
|
||||
var centralContentsY: CGFloat = component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - centralContentsHeight) * 0.5)
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
centralContentsY += iconSize.height + iconTitleSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
}
|
||||
centralContentsY += titleSize.height + titleTextSpacing
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
transition.setPosition(view: textView, position: textFrame.center)
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: component.theme.list.itemCheckColors.fillColor,
|
||||
foreground: component.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: "Add Quick Reply",
|
||||
badge: 0,
|
||||
textColor: component.theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: component.theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: component.theme.list.itemCheckColors.fillColor
|
||||
))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 50.0)
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 8.0 - buttonSize.height), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,563 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Photos
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import MergeLists
|
||||
import ComponentDisplayAdapters
|
||||
import ItemListPeerActionItem
|
||||
import ItemListUI
|
||||
import ChatListUI
|
||||
import QuickReplyNameAlertController
|
||||
|
||||
final class QuickReplySetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
|
||||
init(
|
||||
context: AccountContext
|
||||
) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
static func ==(lhs: QuickReplySetupScreenComponent, rhs: QuickReplySetupScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ShortcutItem: Equatable {
|
||||
var shortcut: String
|
||||
var messages: [EngineMessage]
|
||||
|
||||
init(shortcut: String, messages: [EngineMessage]) {
|
||||
self.shortcut = shortcut
|
||||
self.messages = messages
|
||||
}
|
||||
}
|
||||
|
||||
private enum ContentEntry: Comparable, Identifiable {
|
||||
enum Id: Hashable {
|
||||
case add
|
||||
case item(String)
|
||||
}
|
||||
|
||||
var stableId: Id {
|
||||
switch self {
|
||||
case .add:
|
||||
return .add
|
||||
case let .item(item, _, _):
|
||||
return .item(item.shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
case add
|
||||
case item(item: ShortcutItem, accountPeer: EnginePeer, sortIndex: Int)
|
||||
|
||||
static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .add:
|
||||
return false
|
||||
case let .item(lhsItem, _, lhsSortIndex):
|
||||
switch rhs {
|
||||
case .add:
|
||||
return false
|
||||
case let .item(rhsItem, _, rhsSortIndex):
|
||||
if lhsSortIndex != rhsSortIndex {
|
||||
return lhsSortIndex < rhsSortIndex
|
||||
}
|
||||
return lhsItem.shortcut < rhsItem.shortcut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(listNode: ContentListNode) -> ListViewItem {
|
||||
switch self {
|
||||
case .add:
|
||||
//TODO:localize
|
||||
return ItemListPeerActionItem(
|
||||
presentationData: ItemListPresentationData(listNode.presentationData),
|
||||
icon: PresentationResourcesItemList.plusIconImage(listNode.presentationData.theme),
|
||||
iconSignal: nil,
|
||||
title: "New Quick Reply",
|
||||
additionalBadgeIcon: nil,
|
||||
alwaysPlain: true,
|
||||
hasSeparator: true,
|
||||
sectionId: 0,
|
||||
height: .generic,
|
||||
color: .accent,
|
||||
editing: false,
|
||||
action: { [weak listNode] in
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openQuickReplyChat(shortcut: nil)
|
||||
}
|
||||
)
|
||||
case let .item(item, accountPeer, _):
|
||||
let chatListNodeInteraction = ChatListNodeInteraction(
|
||||
context: listNode.context,
|
||||
animationCache: listNode.context.animationCache,
|
||||
animationRenderer: listNode.context.animationRenderer,
|
||||
activateSearch: {
|
||||
},
|
||||
peerSelected: { [weak listNode] _, _, _, _ in
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openQuickReplyChat(shortcut: item.shortcut)
|
||||
},
|
||||
disabledPeerSelected: { _, _, _ in
|
||||
},
|
||||
togglePeerSelected: { _, _ in
|
||||
},
|
||||
togglePeersSelection: { _, _ in
|
||||
},
|
||||
additionalCategorySelected: { _ in
|
||||
},
|
||||
messageSelected: { [weak listNode] _, _, _, _ in
|
||||
guard let listNode, let parentView = listNode.parentView else {
|
||||
return
|
||||
}
|
||||
parentView.openQuickReplyChat(shortcut: item.shortcut)
|
||||
},
|
||||
groupSelected: { _ in
|
||||
},
|
||||
addContact: { _ in
|
||||
},
|
||||
setPeerIdWithRevealedOptions: { _, _ in
|
||||
},
|
||||
setItemPinned: { _, _ in
|
||||
},
|
||||
setPeerMuted: { _, _ in
|
||||
},
|
||||
setPeerThreadMuted: { _, _, _ in
|
||||
},
|
||||
deletePeer: { _, _ in
|
||||
},
|
||||
deletePeerThread: { _, _ in
|
||||
},
|
||||
setPeerThreadStopped: { _, _, _ in
|
||||
},
|
||||
setPeerThreadPinned: { _, _, _ in
|
||||
},
|
||||
setPeerThreadHidden: { _, _, _ in
|
||||
},
|
||||
updatePeerGrouping: { _, _ in
|
||||
},
|
||||
togglePeerMarkedUnread: { _, _ in
|
||||
},
|
||||
toggleArchivedFolderHiddenByDefault: {
|
||||
},
|
||||
toggleThreadsSelection: { _, _ in
|
||||
},
|
||||
hidePsa: { _ in
|
||||
},
|
||||
activateChatPreview: { _, _, _, _, _ in
|
||||
},
|
||||
present: { _ in
|
||||
},
|
||||
openForumThread: { _, _ in
|
||||
},
|
||||
openStorageManagement: {
|
||||
},
|
||||
openPasswordSetup: {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: {
|
||||
},
|
||||
openActiveSessions: {
|
||||
},
|
||||
performActiveSessionAction: { _, _ in
|
||||
},
|
||||
openChatFolderUpdates: {
|
||||
},
|
||||
hideChatFolderUpdates: {
|
||||
},
|
||||
openStories: { _, _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
}
|
||||
)
|
||||
|
||||
let presentationData = listNode.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let chatListPresentationData = ChatListPresentationData(
|
||||
theme: presentationData.theme,
|
||||
fontSize: presentationData.listsFontSize,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameSortOrder: presentationData.nameSortOrder,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
disableAnimations: false
|
||||
)
|
||||
|
||||
return ChatListItem(
|
||||
presentationData: chatListPresentationData,
|
||||
context: listNode.context,
|
||||
chatListLocation: .chatList(groupId: .root),
|
||||
filterData: nil,
|
||||
index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: listNode.context.account.peerId, namespace: 0, id: 0), timestamp: 0))),
|
||||
content: .peer(ChatListItemContent.PeerData(
|
||||
messages: item.messages.first.flatMap({ [$0] }) ?? [],
|
||||
peer: EngineRenderedPeer(peer: accountPeer),
|
||||
threadInfo: nil,
|
||||
combinedReadState: nil,
|
||||
isRemovedFromTotalUnreadCount: false,
|
||||
presence: nil,
|
||||
hasUnseenMentions: false,
|
||||
hasUnseenReactions: false,
|
||||
draftState: nil,
|
||||
mediaDraftContentType: nil,
|
||||
inputActivities: nil,
|
||||
promoInfo: nil,
|
||||
ignoreUnreadBadge: false,
|
||||
displayAsMessage: false,
|
||||
hasFailedMessages: false,
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
storyState: nil,
|
||||
requiresPremiumForMessaging: false,
|
||||
displayAsTopicList: false,
|
||||
tags: [],
|
||||
customMessageListData: ChatListItemContent.CustomMessageListData(
|
||||
commandPrefix: "/\(item.shortcut)",
|
||||
messageCount: nil
|
||||
)
|
||||
)),
|
||||
editing: false,
|
||||
hasActiveRevealControls: false,
|
||||
selected: false,
|
||||
header: nil,
|
||||
enableContextActions: false,
|
||||
hiddenOffset: false,
|
||||
interaction: chatListNodeInteraction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContentListNode: ListView {
|
||||
weak var parentView: View?
|
||||
let context: AccountContext
|
||||
var presentationData: PresentationData
|
||||
private var currentEntries: [ContentEntry] = []
|
||||
|
||||
init(parentView: View, context: AccountContext) {
|
||||
self.parentView = parentView
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
||||
let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition)
|
||||
self.transaction(
|
||||
deleteIndices: [],
|
||||
insertIndicesAndItems: [],
|
||||
updateIndicesAndItems: [],
|
||||
options: [.Synchronous, .LowLatency],
|
||||
additionalScrollDistance: 0.0,
|
||||
updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve),
|
||||
updateOpaqueState: nil
|
||||
)
|
||||
}
|
||||
|
||||
func setEntries(entries: [ContentEntry], animated: Bool) {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
|
||||
self.currentEntries = entries
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) }
|
||||
|
||||
self.transaction(
|
||||
deleteIndices: deletions,
|
||||
insertIndicesAndItems: insertions,
|
||||
updateIndicesAndItems: updates,
|
||||
options: [.Synchronous, .LowLatency],
|
||||
scrollToItem: nil,
|
||||
stationaryItemRange: nil,
|
||||
updateOpaqueState: nil,
|
||||
completion: { _ in
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var emptyState: ComponentView<Empty>?
|
||||
private var contentListNode: ContentListNode?
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: QuickReplySetupScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var items: [ShortcutItem] = []
|
||||
private var messagesDisposable: Disposable?
|
||||
|
||||
private var accountPeer: EnginePeer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.messagesDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
}
|
||||
|
||||
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func openQuickReplyChat(shortcut: String?) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if let shortcut {
|
||||
let contents = GreetingMessageSetupChatContents(
|
||||
context: component.context,
|
||||
messages: self.items.first(where: { $0.shortcut == shortcut })?.messages ?? [],
|
||||
kind: .quickReplyMessageInput(shortcut: shortcut)
|
||||
)
|
||||
let chatController = component.context.sharedContext.makeChatController(
|
||||
context: component.context,
|
||||
chatLocation: .customChatContents,
|
||||
subject: .customChatContents(contents: contents),
|
||||
botStart: nil,
|
||||
mode: .standard(.default)
|
||||
)
|
||||
chatController.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(chatController)
|
||||
self.messagesDisposable?.dispose()
|
||||
self.messagesDisposable = (contents.messages
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let messages = messages.map(EngineMessage.init)
|
||||
|
||||
if messages.isEmpty {
|
||||
if let index = self.items.firstIndex(where: { $0.shortcut == shortcut }) {
|
||||
self.items.remove(at: index)
|
||||
}
|
||||
} else {
|
||||
if let index = self.items.firstIndex(where: { $0.shortcut == shortcut }) {
|
||||
self.items[index].messages = messages
|
||||
} else {
|
||||
self.items.insert(ShortcutItem(
|
||||
shortcut: shortcut,
|
||||
messages: messages
|
||||
), at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
} else {
|
||||
var completion: ((String?) -> Void)?
|
||||
let alertController = quickReplyNameAlertController(
|
||||
context: component.context,
|
||||
text: "New Quick Reply",
|
||||
subtext: "Add a shortcut for your quick reply.",
|
||||
value: "",
|
||||
characterLimit: 32,
|
||||
apply: { value in
|
||||
completion?(value)
|
||||
}
|
||||
)
|
||||
completion = { [weak self, weak alertController] value in
|
||||
guard let self else {
|
||||
alertController?.dismissAnimated()
|
||||
return
|
||||
}
|
||||
if let value, !value.isEmpty {
|
||||
alertController?.dismissAnimated()
|
||||
self.openQuickReplyChat(shortcut: value)
|
||||
}
|
||||
}
|
||||
self.environment?.controller()?.present(alertController, in: .window(.root))
|
||||
}
|
||||
|
||||
self.contentListNode?.clearHighlightAnimated(true)
|
||||
}
|
||||
|
||||
func update(component: QuickReplySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
let _ = (component.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.accountPeer = peer
|
||||
})
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let alphaTransition: Transition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut))
|
||||
let _ = alphaTransition
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.plainBackgroundColor
|
||||
}
|
||||
|
||||
if self.items.isEmpty {
|
||||
let emptyState: ComponentView<Empty>
|
||||
var emptyStateTransition = transition
|
||||
if let current = self.emptyState {
|
||||
emptyState = current
|
||||
} else {
|
||||
emptyState = ComponentView()
|
||||
self.emptyState = emptyState
|
||||
emptyStateTransition = emptyStateTransition.withAnimation(.none)
|
||||
}
|
||||
|
||||
let emptyStateFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight))
|
||||
let _ = emptyState.update(
|
||||
transition: emptyStateTransition,
|
||||
component: AnyComponent(QuickReplyEmptyStateComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openQuickReplyChat(shortcut: nil)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: emptyStateFrame.size
|
||||
)
|
||||
if let emptyStateView = emptyState.view {
|
||||
if emptyStateView.superview == nil {
|
||||
self.addSubview(emptyStateView)
|
||||
}
|
||||
emptyStateTransition.setFrame(view: emptyStateView, frame: emptyStateFrame)
|
||||
}
|
||||
} else {
|
||||
if let emptyState = self.emptyState {
|
||||
self.emptyState = nil
|
||||
emptyState.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let contentListNode: ContentListNode
|
||||
if let current = self.contentListNode {
|
||||
contentListNode = current
|
||||
} else {
|
||||
contentListNode = ContentListNode(parentView: self, context: component.context)
|
||||
self.contentListNode = contentListNode
|
||||
self.addSubview(contentListNode.view)
|
||||
}
|
||||
|
||||
transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), transition: transition)
|
||||
|
||||
var entries: [ContentEntry] = []
|
||||
if let accountPeer = self.accountPeer {
|
||||
entries.append(.add)
|
||||
for item in self.items {
|
||||
entries.append(.item(item: item, accountPeer: accountPeer, sortIndex: entries.count))
|
||||
}
|
||||
}
|
||||
contentListNode.setEntries(entries: entries, animated: false)
|
||||
|
||||
contentListNode.isHidden = self.items.isEmpty
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class QuickReplySetupScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: QuickReplySetupScreenComponent(
|
||||
context: context
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
//TODO:localize
|
||||
self.title = "Quick Replies"
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? QuickReplySetupScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
componentView.scrollToTop()
|
||||
}
|
||||
|
||||
self.attemptNavigation = { [weak self] complete in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? QuickReplySetupScreenComponent.View else {
|
||||
return true
|
||||
}
|
||||
|
||||
return componentView.attemptNavigation(complete: complete)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "QuickReplyNameAlertController",
|
||||
module_name = "QuickReplyNameAlertController",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,508 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import EmojiStatusComponent
|
||||
|
||||
private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
private let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
private let characterLimitView = ComponentView<Empty>()
|
||||
|
||||
private let characterLimit: Int
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
var textChanged: ((String) -> Void)?
|
||||
|
||||
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||
private let inputInsets: UIEdgeInsets
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textInputNode.attributedText?.string ?? ""
|
||||
}
|
||||
set {
|
||||
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String = "" {
|
||||
didSet {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String, characterLimit: Int) {
|
||||
self.theme = theme
|
||||
self.characterLimit = characterLimit
|
||||
|
||||
self.inputInsets = UIEdgeInsets(top: 9.0, left: 6.0, bottom: 9.0, right: 16.0)
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
|
||||
self.textInputNode = EditableTextNode()
|
||||
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(13.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.clipsToBounds = true
|
||||
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: self.inputInsets.left, bottom: self.inputInsets.bottom, right: self.inputInsets.right)
|
||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.keyboardType = .default
|
||||
self.textInputNode.autocapitalizationType = .none
|
||||
self.textInputNode.returnKeyType = .done
|
||||
self.textInputNode.autocorrectionType = .no
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.delegate = self
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
|
||||
|
||||
let characterLimitString: String
|
||||
let characterLimitColor: UIColor
|
||||
if self.text.count <= self.characterLimit {
|
||||
let remaining = self.characterLimit - self.text.count
|
||||
if remaining < 5 {
|
||||
characterLimitString = "\(remaining)"
|
||||
} else {
|
||||
characterLimitString = " "
|
||||
}
|
||||
characterLimitColor = self.theme.list.itemPlaceholderTextColor
|
||||
} else {
|
||||
characterLimitString = "\(self.characterLimit - self.text.count)"
|
||||
characterLimitColor = self.theme.list.itemDestructiveColor
|
||||
}
|
||||
|
||||
let characterLimitSize = self.characterLimitView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: characterLimitString, font: Font.regular(13.0), textColor: characterLimitColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let characterLimitComponentView = self.characterLimitView.view {
|
||||
if characterLimitComponentView.superview == nil {
|
||||
self.view.addSubview(characterLimitComponentView)
|
||||
}
|
||||
characterLimitComponentView.frame = CGRect(origin: CGPoint(x: width - 23.0 - characterLimitSize.width, y: 18.0), size: characterLimitSize)
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.textInputNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
self.textInputNode.resignFirstResponder()
|
||||
}
|
||||
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(editableTextNode.textView.text)
|
||||
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(34.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(34.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
self.textInputNode.attributedText = nil
|
||||
self.deactivateInput()
|
||||
}
|
||||
}
|
||||
|
||||
private final class QuickReplyNameAlertContentNode: AlertContentNode {
|
||||
private let context: AccountContext
|
||||
private var theme: AlertControllerTheme
|
||||
private let strings: PresentationStrings
|
||||
private let text: String
|
||||
private let subtext: String
|
||||
private let titleFont: PromptControllerTitleFont
|
||||
|
||||
private let textView = ComponentView<Empty>()
|
||||
private let subtextView = ComponentView<Empty>()
|
||||
|
||||
let inputFieldNode: PromptInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, subtext: String, titleFont: PromptControllerTitleFont, value: String?, characterLimit: Int) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.text = text
|
||||
self.subtext = subtext
|
||||
self.titleFont = titleFont
|
||||
|
||||
//TODO:localize
|
||||
self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "Shortcut", characterLimit: characterLimit)
|
||||
self.inputFieldNode.text = value ?? ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = true
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.inputFieldNode.textChanged = { [weak self] text in
|
||||
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
|
||||
lastNode.actionEnabled = text.count <= characterLimit
|
||||
strongSelf.requestLayout?(.immediate)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var value: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 16.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
let subtextSpacing: CGFloat = -1.0
|
||||
|
||||
let textSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: measureSize.width, height: 1000.0)
|
||||
)
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: origin.y), size: textSize)
|
||||
if let textComponentView = self.textView.view {
|
||||
if textComponentView.superview == nil {
|
||||
self.view.addSubview(textComponentView)
|
||||
}
|
||||
textComponentView.frame = textFrame
|
||||
}
|
||||
origin.y += textSize.height + 6.0 + subtextSpacing
|
||||
|
||||
let subtextSize = self.subtextView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: self.subtext, font: Font.regular(13.0), textColor: self.theme.primaryColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: measureSize.width, height: 1000.0)
|
||||
)
|
||||
let subtextFrame = CGRect(origin: CGPoint(x: floor((size.width - subtextSize.width) * 0.5), y: origin.y), size: subtextSize)
|
||||
if let subtextComponentView = self.subtextView.view {
|
||||
if subtextComponentView.superview == nil {
|
||||
self.view.addSubview(subtextComponentView)
|
||||
}
|
||||
subtextComponentView.frame = subtextFrame
|
||||
}
|
||||
origin.y += subtextSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(textSize.width, minActionsWidth)
|
||||
contentWidth = max(subtextSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
let inputFieldFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: inputFieldFrame)
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: textSize.height + subtextSpacing + subtextSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
public enum PromptControllerTitleFont {
|
||||
case regular
|
||||
case bold
|
||||
}
|
||||
|
||||
public func quickReplyNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = QuickReplyNameAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, subtext: subtext, titleFont: titleFont, value: value, characterLimit: characterLimit)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
apply(contentNode.value)
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
contentNode.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "TimezoneSelectionScreen",
|
||||
module_name = "TimezoneSelectionScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/SearchUI",
|
||||
"//submodules/MergeLists",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/SearchBarNode",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,156 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import SearchUI
|
||||
|
||||
public class TimezoneSelectionScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
private let completed: (String) -> Void
|
||||
|
||||
private var controllerNode: TimezoneSelectionScreenNode {
|
||||
return self.displayNode as! TimezoneSelectionScreenNode
|
||||
}
|
||||
|
||||
private var _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
|
||||
private var previousContentOffset: ListViewVisibleContentOffset?
|
||||
|
||||
public init(context: AccountContext, completed: @escaping (String) -> Void) {
|
||||
self.context = context
|
||||
self.completed = completed
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
//TODO:localize
|
||||
self.title = "Time Zone"
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let searchContentNode = strongSelf.searchContentNode {
|
||||
searchContentNode.updateExpansionProgress(1.0, animated: true)
|
||||
}
|
||||
strongSelf.controllerNode.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
let previousTheme = strongSelf.presentationData.theme
|
||||
let previousStrings = strongSelf.presentationData.strings
|
||||
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
||||
strongSelf.updateThemeAndStrings()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, inline: true, activate: { [weak self] in
|
||||
self?.activateSearch()
|
||||
})
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
|
||||
self.title = self.presentationData.strings.Settings_AppLanguage
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
self.controllerNode.updatePresentationData(self.presentationData)
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = TimezoneSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in
|
||||
self?.activateSearch()
|
||||
}, requestDeactivateSearch: { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
}, action: { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.completed(id)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, push: { [weak self] c in
|
||||
self?.push(c)
|
||||
})
|
||||
|
||||
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in
|
||||
if let strongSelf = self {
|
||||
if let searchContentNode = strongSelf.searchContentNode {
|
||||
searchContentNode.updateListVisibleContentOffset(offset)
|
||||
}
|
||||
|
||||
strongSelf.previousContentOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.listNode.didEndScrolling = { [weak self] _ in
|
||||
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
|
||||
let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode)
|
||||
}
|
||||
}
|
||||
|
||||
self._ready.set(self.controllerNode._ready.get())
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
private func activateSearch() {
|
||||
if self.displayNavigationBar {
|
||||
if let scrollToTop = self.scrollToTop {
|
||||
scrollToTop()
|
||||
}
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
|
||||
}
|
||||
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
private func deactivateSearch() {
|
||||
if !self.displayNavigationBar {
|
||||
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,477 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import SearchBarNode
|
||||
import SearchUI
|
||||
import TelegramUIPreferences
|
||||
|
||||
private struct TimezoneListEntry: Comparable, Identifiable {
|
||||
var id: String
|
||||
var offset: Int
|
||||
var title: String
|
||||
|
||||
var stableId: String {
|
||||
return self.id
|
||||
}
|
||||
|
||||
static func <(lhs: TimezoneListEntry, rhs: TimezoneListEntry) -> Bool {
|
||||
if lhs.offset != rhs.offset {
|
||||
return lhs.offset < rhs.offset
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return lhs.title < rhs.title
|
||||
}
|
||||
return lhs.id < rhs.id
|
||||
}
|
||||
|
||||
func item(presentationData: PresentationData, searchMode: Bool, action: @escaping (String) -> Void) -> ListViewItem {
|
||||
return ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: self.title, kind: .neutral, alignment: .natural, sectionId: 0, style: .plain, action: {
|
||||
action(self.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private struct TimezoneListSearchContainerTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let isSearching: Bool
|
||||
}
|
||||
|
||||
private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [TimezoneListEntry], to toEntries: [TimezoneListEntry], action: @escaping (String) -> Void, isSearching: Bool, forceUpdate: Bool) -> TimezoneListSearchContainerTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, action: action), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, action: action), directionHint: nil) }
|
||||
|
||||
return TimezoneListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
|
||||
}
|
||||
|
||||
private final class TimezoneListSearchContainerNode: SearchDisplayControllerContentNode {
|
||||
private let timezoneData: TimezoneData
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let listNode: ListView
|
||||
|
||||
private var enqueuedTransitions: [TimezoneListSearchContainerTransition] = []
|
||||
private var hasValidLayout = false
|
||||
|
||||
private let searchQuery = Promise<String?>()
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private let presentationDataPromise: Promise<PresentationData>
|
||||
|
||||
public override var hasDim: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(context: AccountContext, timezoneData: TimezoneData, action: @escaping (String) -> Void) {
|
||||
self.timezoneData = timezoneData
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.presentationDataPromise = Promise(self.presentationData)
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
|
||||
self.listNode.isHidden = true
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
let foundItems = self.searchQuery.get()
|
||||
|> mapToSignal { query -> Signal<[TimezoneData.Item]?, NoError> in
|
||||
if let query, !query.isEmpty {
|
||||
let query = query.lowercased()
|
||||
|
||||
return .single(timezoneData.items.filter { item in
|
||||
if item.id.lowercased().hasPrefix(query) {
|
||||
return true
|
||||
}
|
||||
if item.title.lowercased().split(separator: " ").contains(where: { $0.hasPrefix(query) }) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|
||||
let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
|
||||
self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get()).start(next: { [weak self] items, presentationData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var entries: [TimezoneListEntry] = []
|
||||
if let items {
|
||||
for item in items {
|
||||
entries.append(TimezoneListEntry(
|
||||
id: item.id,
|
||||
offset: item.offset,
|
||||
title: item.title
|
||||
))
|
||||
}
|
||||
}
|
||||
entries.sort()
|
||||
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
|
||||
let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, action: action, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}))
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
let previousTheme = strongSelf.presentationData.theme
|
||||
let previousStrings = strongSelf.presentationData.strings
|
||||
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
||||
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
|
||||
strongSelf.presentationDataPromise.set(.single(presentationData))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.listNode.beganInteractiveDragging = { [weak self] _ in
|
||||
self?.dismissInput?()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.listNode.backgroundColor = theme.chatList.backgroundColor
|
||||
}
|
||||
|
||||
override func searchTextUpdated(text: String) {
|
||||
if text.isEmpty {
|
||||
self.searchQuery.set(.single(nil))
|
||||
} else {
|
||||
self.searchQuery.set(.single(text))
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: TimezoneListSearchContainerTransition) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
|
||||
if self.hasValidLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransitions() {
|
||||
if let transition = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
|
||||
let isSearching = transition.isSearching
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
self?.listNode.isHidden = !isSearching
|
||||
self?.dimNode.isHidden = isSearching
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
|
||||
let topInset = navigationBarHeight
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
|
||||
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !self.hasValidLayout {
|
||||
self.hasValidLayout = true
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.cancel?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TimezoneListNodeTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let firstTime: Bool
|
||||
let isLoading: Bool
|
||||
let animated: Bool
|
||||
let crossfade: Bool
|
||||
}
|
||||
|
||||
private func preparedTimezoneListNodeTransition(presentationData: PresentationData, from fromEntries: [TimezoneListEntry], to toEntries: [TimezoneListEntry], action: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> TimezoneListNodeTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, action: action), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, action: action), directionHint: nil) }
|
||||
|
||||
return TimezoneListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade)
|
||||
}
|
||||
|
||||
private final class TimezoneData {
|
||||
struct Item {
|
||||
var id: String
|
||||
var offset: Int
|
||||
var title: String
|
||||
|
||||
init(id: String, offset: Int, title: String) {
|
||||
self.id = id
|
||||
self.offset = offset
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
let items: [Item]
|
||||
|
||||
init() {
|
||||
let locale = Locale.current
|
||||
var items: [Item] = []
|
||||
for (key, value) in TimeZone.abbreviationDictionary {
|
||||
guard let timezone = TimeZone(abbreviation: key) else {
|
||||
continue
|
||||
}
|
||||
if items.contains(where: { $0.id == timezone.identifier }) {
|
||||
continue
|
||||
}
|
||||
items.append(Item(
|
||||
id: timezone.identifier,
|
||||
offset: timezone.secondsFromGMT(),
|
||||
title: timezone.localizedName(for: .standard, locale: locale) ?? value
|
||||
))
|
||||
}
|
||||
self.items = items
|
||||
}
|
||||
}
|
||||
|
||||
final class TimezoneSelectionScreenNode: ViewControllerTracingNode {
|
||||
private let context: AccountContext
|
||||
private let action: (String) -> Void
|
||||
private var presentationData: PresentationData
|
||||
private weak var navigationBar: NavigationBar?
|
||||
private let requestActivateSearch: () -> Void
|
||||
private let requestDeactivateSearch: () -> Void
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
private let push: (ViewController) -> Void
|
||||
private let timezoneData: TimezoneData
|
||||
|
||||
private var didSetReady = false
|
||||
let _ready = ValuePromise<Bool>()
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
let listNode: ListView
|
||||
private var queuedTransitions: [TimezoneListNodeTransition] = []
|
||||
private var searchDisplayController: SearchDisplayController?
|
||||
|
||||
private let presentationDataValue = Promise<PresentationData>()
|
||||
|
||||
private var listDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, action: @escaping (String) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
self.action = action
|
||||
self.presentationData = presentationData
|
||||
self.presentationDataValue.set(.single(presentationData))
|
||||
self.navigationBar = navigationBar
|
||||
self.requestActivateSearch = requestActivateSearch
|
||||
self.requestDeactivateSearch = requestDeactivateSearch
|
||||
self.present = present
|
||||
self.push = push
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
let timezoneData = TimezoneData()
|
||||
self.timezoneData = timezoneData
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.addSubnode(self.listNode)
|
||||
|
||||
let previousEntriesHolder = Atomic<([TimezoneListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
|
||||
self.listDisposable = (self.presentationDataValue.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var entries: [TimezoneListEntry] = []
|
||||
for item in timezoneData.items {
|
||||
entries.append(TimezoneListEntry(
|
||||
id: item.id,
|
||||
offset: item.offset,
|
||||
title: item.title
|
||||
))
|
||||
}
|
||||
entries.sort()
|
||||
|
||||
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
|
||||
let transition = preparedTimezoneListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, action: action, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: false, crossfade: false)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.listDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
let stringsUpdated = self.presentationData.strings !== presentationData.strings
|
||||
self.presentationData = presentationData
|
||||
|
||||
if stringsUpdated {
|
||||
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
|
||||
self.view.addSubview(snapshotView)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.presentationDataValue.set(.single(presentationData))
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.searchDisplayController?.updatePresentationData(presentationData)
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let hadValidLayout = self.containerLayout != nil
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var listInsets = layout.insets(options: [.input])
|
||||
listInsets.top += navigationBarHeight
|
||||
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
}
|
||||
|
||||
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve)
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if !hadValidLayout {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: TimezoneListNodeTransition) {
|
||||
self.queuedTransitions.append(transition)
|
||||
|
||||
if self.containerLayout != nil {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransitions() {
|
||||
guard let _ = self.containerLayout else {
|
||||
return
|
||||
}
|
||||
while !self.queuedTransitions.isEmpty {
|
||||
let transition = self.queuedTransitions.removeFirst()
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if transition.firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
} else if transition.crossfade {
|
||||
options.insert(.AnimateCrossfade)
|
||||
} else if transition.animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.didSetReady {
|
||||
strongSelf.didSetReady = true
|
||||
strongSelf._ready.set(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
|
||||
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: TimezoneListSearchContainerNode(context: self.context, timezoneData: self.timezoneData, action: self.action), inline: true, cancel: { [weak self] in
|
||||
self?.requestDeactivateSearch()
|
||||
})
|
||||
|
||||
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
|
||||
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
|
||||
if isSearchBar {
|
||||
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
|
||||
} else if let navigationBar = strongSelf.navigationBar {
|
||||
strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
|
||||
}
|
||||
}
|
||||
}, placeholder: placeholderNode)
|
||||
}
|
||||
|
||||
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
searchDisplayController.deactivate(placeholder: placeholderNode)
|
||||
self.searchDisplayController = nil
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
}
|
@ -13,9 +13,8 @@ swift_library(
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/LegacyUI",
|
||||
"//submodules/LegacyComponents",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -3,96 +3,69 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import LegacyUI
|
||||
import LegacyComponents
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
|
||||
final class SliderComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
public final class SliderComponent: Component {
|
||||
public let valueCount: Int
|
||||
public let value: Int
|
||||
public let trackBackgroundColor: UIColor
|
||||
public let trackForegroundColor: UIColor
|
||||
public let valueUpdated: (Int) -> Void
|
||||
public let isTrackingUpdated: ((Bool) -> Void)?
|
||||
|
||||
let title: String
|
||||
let value: Float
|
||||
let minValue: Float
|
||||
let maxValue: Float
|
||||
let startValue: Float
|
||||
let isEnabled: Bool
|
||||
let trackColor: UIColor?
|
||||
let displayValue: Bool
|
||||
let valueUpdated: (Float) -> Void
|
||||
let isTrackingUpdated: ((Bool) -> Void)?
|
||||
|
||||
init(
|
||||
title: String,
|
||||
value: Float,
|
||||
minValue: Float,
|
||||
maxValue: Float,
|
||||
startValue: Float,
|
||||
isEnabled: Bool,
|
||||
trackColor: UIColor?,
|
||||
displayValue: Bool,
|
||||
valueUpdated: @escaping (Float) -> Void,
|
||||
public init(
|
||||
valueCount: Int,
|
||||
value: Int,
|
||||
trackBackgroundColor: UIColor,
|
||||
trackForegroundColor: UIColor,
|
||||
valueUpdated: @escaping (Int) -> Void,
|
||||
isTrackingUpdated: ((Bool) -> Void)? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.valueCount = valueCount
|
||||
self.value = value
|
||||
self.minValue = minValue
|
||||
self.maxValue = maxValue
|
||||
self.startValue = startValue
|
||||
self.isEnabled = isEnabled
|
||||
self.trackColor = trackColor
|
||||
self.displayValue = displayValue
|
||||
self.trackBackgroundColor = trackBackgroundColor
|
||||
self.trackForegroundColor = trackForegroundColor
|
||||
self.valueUpdated = valueUpdated
|
||||
self.isTrackingUpdated = isTrackingUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
public static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool {
|
||||
if lhs.valueCount != rhs.valueCount {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
if lhs.minValue != rhs.minValue {
|
||||
if lhs.trackBackgroundColor != rhs.trackBackgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.maxValue != rhs.maxValue {
|
||||
return false
|
||||
}
|
||||
if lhs.startValue != rhs.startValue {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.trackColor != rhs.trackColor {
|
||||
return false
|
||||
}
|
||||
if lhs.displayValue != rhs.displayValue {
|
||||
if lhs.trackForegroundColor != rhs.trackForegroundColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UITextFieldDelegate {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let value = ComponentView<Empty>()
|
||||
public final class View: UIView {
|
||||
private var sliderView: TGPhotoEditorSliderView?
|
||||
|
||||
private var component: SliderComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 44.0)
|
||||
|
||||
var internalIsTrackingUpdated: ((Bool) -> Void)?
|
||||
if let isTrackingUpdated = component.isTrackingUpdated {
|
||||
internalIsTrackingUpdated = { [weak self] isTracking in
|
||||
@ -100,23 +73,11 @@ final class SliderComponent: Component {
|
||||
if isTracking {
|
||||
self.sliderView?.bordered = true
|
||||
} else {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.sliderView?.bordered = false
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
|
||||
self?.sliderView?.bordered = false
|
||||
})
|
||||
}
|
||||
isTrackingUpdated(isTracking)
|
||||
let transition: Transition
|
||||
if isTracking {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
if let titleView = self.title.view {
|
||||
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
if let valueView = self.value.view {
|
||||
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -124,24 +85,42 @@ final class SliderComponent: Component {
|
||||
let sliderView: TGPhotoEditorSliderView
|
||||
if let current = self.sliderView {
|
||||
sliderView = current
|
||||
sliderView.value = CGFloat(component.value)
|
||||
} else {
|
||||
sliderView = TGPhotoEditorSliderView()
|
||||
sliderView.backgroundColor = .clear
|
||||
sliderView.startColor = UIColor(rgb: 0xffffff)
|
||||
sliderView.enablePanHandling = true
|
||||
sliderView.trackCornerRadius = 1.0
|
||||
sliderView.lineSize = 2.0
|
||||
sliderView.minimumValue = CGFloat(component.minValue)
|
||||
sliderView.maximumValue = CGFloat(component.maxValue)
|
||||
sliderView.startValue = CGFloat(component.startValue)
|
||||
sliderView.value = CGFloat(component.value)
|
||||
sliderView.trackCornerRadius = 2.0
|
||||
sliderView.lineSize = 4.0
|
||||
sliderView.dotSize = 5.0
|
||||
sliderView.minimumValue = 0.0
|
||||
sliderView.startValue = 0.0
|
||||
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
sliderView.maximumValue = CGFloat(component.valueCount)
|
||||
sliderView.positionsCount = component.valueCount + 1
|
||||
sliderView.useLinesForPositions = true
|
||||
|
||||
sliderView.backgroundColor = nil
|
||||
sliderView.isOpaque = false
|
||||
sliderView.backColor = component.trackBackgroundColor
|
||||
sliderView.startColor = component.trackBackgroundColor
|
||||
sliderView.trackColor = component.trackForegroundColor
|
||||
sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
})
|
||||
|
||||
sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
|
||||
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
|
||||
|
||||
|
||||
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
|
||||
sliderView.layer.allowsGroupOpacity = true
|
||||
self.sliderView = sliderView
|
||||
self.addSubview(sliderView)
|
||||
}
|
||||
sliderView.value = CGFloat(component.value)
|
||||
sliderView.interactionBegan = {
|
||||
internalIsTrackingUpdated?(true)
|
||||
}
|
||||
@ -149,70 +128,17 @@ final class SliderComponent: Component {
|
||||
internalIsTrackingUpdated?(false)
|
||||
}
|
||||
|
||||
if component.isEnabled {
|
||||
sliderView.alpha = 1.3
|
||||
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
|
||||
sliderView.isUserInteractionEnabled = true
|
||||
} else {
|
||||
sliderView.trackColor = UIColor(rgb: 0xffffff)
|
||||
sliderView.alpha = 0.3
|
||||
sliderView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: 44.0)))
|
||||
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
|
||||
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
|
||||
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
|
||||
}
|
||||
|
||||
let valueText: String
|
||||
if component.displayValue {
|
||||
if component.value > 0.005 {
|
||||
valueText = String(format: "+%.2f", component.value)
|
||||
} else if component.value < -0.005 {
|
||||
valueText = String(format: "%.2f", component.value)
|
||||
} else {
|
||||
valueText = ""
|
||||
}
|
||||
} else {
|
||||
valueText = ""
|
||||
}
|
||||
|
||||
let valueSize = self.value.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xf8d74a))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let valueView = self.value.view {
|
||||
if valueView.superview == nil {
|
||||
self.addSubview(valueView)
|
||||
}
|
||||
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: 52.0)
|
||||
return size
|
||||
}
|
||||
|
||||
@objc private func sliderValueChanged() {
|
||||
guard let component = self.component, let sliderView = self.sliderView else {
|
||||
return
|
||||
}
|
||||
component.valueUpdated(Float(sliderView.value))
|
||||
component.valueUpdated(Int(sliderView.value))
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,240 +146,7 @@ final class SliderComponent: Component {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
struct AdjustmentTool: Equatable {
|
||||
let key: EditorToolKey
|
||||
let title: String
|
||||
let value: Float
|
||||
let minValue: Float
|
||||
let maxValue: Float
|
||||
let startValue: Float
|
||||
}
|
||||
|
||||
final class AdjustmentsComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let tools: [AdjustmentTool]
|
||||
let valueUpdated: (EditorToolKey, Float) -> Void
|
||||
let isTrackingUpdated: (Bool) -> Void
|
||||
|
||||
init(
|
||||
tools: [AdjustmentTool],
|
||||
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
|
||||
isTrackingUpdated: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.tools = tools
|
||||
self.valueUpdated = valueUpdated
|
||||
self.isTrackingUpdated = isTrackingUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
|
||||
if lhs.tools != rhs.tools {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let scrollView = UIScrollView()
|
||||
private var toolViews: [ComponentView<Empty>] = []
|
||||
|
||||
private var component: AdjustmentsComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let valueUpdated = component.valueUpdated
|
||||
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
|
||||
component.isTrackingUpdated(isTracking)
|
||||
|
||||
if let self {
|
||||
for i in 0 ..< component.tools.count {
|
||||
let tool = component.tools[i]
|
||||
if tool.key != trackingTool && i < self.toolViews.count {
|
||||
if let view = self.toolViews[i].view {
|
||||
let transition: Transition
|
||||
if isTracking {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sizes: [CGSize] = []
|
||||
for i in 0 ..< component.tools.count {
|
||||
let tool = component.tools[i]
|
||||
let componentView: ComponentView<Empty>
|
||||
if i >= self.toolViews.count {
|
||||
componentView = ComponentView<Empty>()
|
||||
self.toolViews.append(componentView)
|
||||
} else {
|
||||
componentView = self.toolViews[i]
|
||||
}
|
||||
|
||||
var valueIsNegative = false
|
||||
var value = tool.value
|
||||
if case .enhance = tool.key {
|
||||
if value < 0.0 {
|
||||
valueIsNegative = true
|
||||
}
|
||||
value = abs(value)
|
||||
}
|
||||
|
||||
let size = componentView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SliderComponent(
|
||||
title: tool.title,
|
||||
value: value,
|
||||
minValue: tool.minValue,
|
||||
maxValue: tool.maxValue,
|
||||
startValue: tool.startValue,
|
||||
isEnabled: true,
|
||||
trackColor: nil,
|
||||
displayValue: true,
|
||||
valueUpdated: { value in
|
||||
var updatedValue = value
|
||||
if valueIsNegative {
|
||||
updatedValue *= -1.0
|
||||
}
|
||||
valueUpdated(tool.key, updatedValue)
|
||||
},
|
||||
isTrackingUpdated: { isTracking in
|
||||
isTrackingUpdated(tool.key, isTracking)
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
sizes.append(size)
|
||||
}
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
|
||||
for i in 0 ..< component.tools.count {
|
||||
let size = sizes[i]
|
||||
let componentView = self.toolViews[i]
|
||||
|
||||
if let view = componentView.view {
|
||||
if view.superview == nil {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
|
||||
}
|
||||
origin = origin.offsetBy(dx: 0.0, dy: size.height)
|
||||
}
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 180.0)
|
||||
let contentSize = CGSize(width: availableSize.width, height: origin.y)
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class AdjustmentsScreenComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let toggleUneditedPreview: (Bool) -> Void
|
||||
|
||||
init(
|
||||
toggleUneditedPreview: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.toggleUneditedPreview = toggleUneditedPreview
|
||||
}
|
||||
|
||||
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
enum Field {
|
||||
case blacks
|
||||
case shadows
|
||||
case midtones
|
||||
case highlights
|
||||
case whites
|
||||
}
|
||||
|
||||
private var component: AdjustmentsScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
|
||||
longPressGestureRecognizer.minimumPressDuration = 0.05
|
||||
self.addGestureRecognizer(longPressGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
component.toggleUneditedPreview(true)
|
||||
case .ended, .cancelled:
|
||||
component.toggleUneditedPreview(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "TimeSelectionActionSheet",
|
||||
module_name = "TimeSelectionActionSheet",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -9,15 +9,15 @@ import TelegramStringFormatting
|
||||
import AccountContext
|
||||
import UIKitRuntimeUtils
|
||||
|
||||
final class TimeSelectionActionSheet: ActionSheetController {
|
||||
public final class TimeSelectionActionSheet: ActionSheetController {
|
||||
private var presentationDisposable: Disposable?
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override var ready: Promise<Bool> {
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
|
||||
public init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
@ -56,7 +56,7 @@ final class TimeSelectionActionSheet: ActionSheetController {
|
||||
])
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
BIN
submodules/TelegramUI/Resources/Animations/ZzzEmoji.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/ZzzEmoji.tgs
Normal file
Binary file not shown.
@ -483,7 +483,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||||
return .thread(peerId: data.peerId, threadId: data.threadId, data: context.state)
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
@ -509,7 +509,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
@ -547,7 +547,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||||
return context.unreadCount
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return .single(0)
|
||||
}
|
||||
}
|
||||
@ -559,7 +559,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
case let .replyThread(data):
|
||||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||||
context.applyMaxReadIndex(messageIndex: messageIndex)
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +289,7 @@ extension ChatControllerImpl {
|
||||
if let location = location {
|
||||
source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
|
||||
} else {
|
||||
source = .extracted(ChatMessageContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll))
|
||||
source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll))
|
||||
}
|
||||
|
||||
self.canReadHistory.set(false)
|
||||
|
@ -137,7 +137,7 @@ public final class ChatControllerOverlayPresentationData {
|
||||
enum ChatLocationInfoData {
|
||||
case peer(Promise<PeerView>)
|
||||
case replyThread(Promise<Message?>)
|
||||
case feed
|
||||
case customChatContents
|
||||
}
|
||||
|
||||
enum ChatRecordingActivity {
|
||||
@ -647,10 +647,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
promise.set(.single(nil))
|
||||
}
|
||||
self.chatLocationInfoData = .replyThread(promise)
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
locationBroadcastPanelSource = .none
|
||||
groupCallPanelSource = .none
|
||||
self.chatLocationInfoData = .feed
|
||||
self.chatLocationInfoData = .customChatContents
|
||||
}
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -2296,7 +2296,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
case .replyThread:
|
||||
postAsReply = true
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
postAsReply = true
|
||||
}
|
||||
|
||||
@ -2902,7 +2902,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
case let .replyThread(replyThreadMessage):
|
||||
let peerId = replyThreadMessage.peerId
|
||||
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil)
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
}, requestRedeliveryOfFailedMessages: { [weak self] id in
|
||||
@ -2943,7 +2943,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
||||
|
||||
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
|
||||
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
|
||||
strongSelf.currentContextController = controller
|
||||
strongSelf.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
@ -3021,7 +3021,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
||||
|
||||
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
|
||||
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
|
||||
strongSelf.currentContextController = controller
|
||||
strongSelf.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
@ -4787,7 +4787,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
|
||||
self.avatarNode = avatarNode
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
|
||||
}
|
||||
chatInfoButtonItem.target = self
|
||||
@ -5669,7 +5669,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
replyThreadType = .replies
|
||||
}
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
replyThreadType = .replies
|
||||
}
|
||||
|
||||
@ -6149,11 +6149,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else if case .feed = self.chatLocationInfoData {
|
||||
} else if case .customChatContents = self.chatLocationInfoData {
|
||||
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
|
||||
self.titleDisposable.set(nil)
|
||||
|
||||
self.chatTitleView?.titleContent = .custom("Feed", nil, false)
|
||||
//TODO:localize
|
||||
if case let .customChatContents(customChatContents) = self.subject {
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput:
|
||||
self.chatTitleView?.titleContent = .custom("Greeting Message", nil, false)
|
||||
case .awayMessageInput:
|
||||
self.chatTitleView?.titleContent = .custom("Away Message", nil, false)
|
||||
case let .quickReplyMessageInput(shortcut):
|
||||
self.chatTitleView?.titleContent = .custom("/\(shortcut)", nil, false)
|
||||
}
|
||||
} else {
|
||||
self.chatTitleView?.titleContent = .custom("Messages", nil, false)
|
||||
}
|
||||
|
||||
if !self.didSetChatLocationInfoReady {
|
||||
self.didSetChatLocationInfoReady = true
|
||||
@ -6307,7 +6319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
|
||||
case let .replyThread(replyThreadMessage):
|
||||
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
activitySpace = nil
|
||||
}
|
||||
|
||||
@ -7763,7 +7775,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
case .peer:
|
||||
pinnedMessageId = topPinnedMessage?.message.id
|
||||
pinnedMessage = topPinnedMessage
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
pinnedMessageId = nil
|
||||
pinnedMessage = nil
|
||||
}
|
||||
@ -7934,7 +7946,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
$0.updatedChatHistoryState(state)
|
||||
})
|
||||
|
||||
if let botStart = strongSelf.botStart, case let .loaded(isEmpty) = state {
|
||||
if let botStart = strongSelf.botStart, case let .loaded(isEmpty, _) = state {
|
||||
strongSelf.botStart = nil
|
||||
if !isEmpty {
|
||||
strongSelf.startBot(botStart.payload)
|
||||
@ -8171,20 +8183,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in
|
||||
if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId {
|
||||
var correlationIds: [Int64] = []
|
||||
for message in messages {
|
||||
switch message {
|
||||
case let .message(_, _, _, _, _, _, _, _, correlationId, _):
|
||||
if let correlationId = correlationId {
|
||||
correlationIds.append(correlationId)
|
||||
}
|
||||
default:
|
||||
break
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var correlationIds: [Int64] = []
|
||||
for message in messages {
|
||||
switch message {
|
||||
case let .message(_, _, _, _, _, _, _, _, correlationId, _):
|
||||
if let correlationId = correlationId {
|
||||
correlationIds.append(correlationId)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
strongSelf.commitPurposefulAction()
|
||||
|
||||
}
|
||||
strongSelf.commitPurposefulAction()
|
||||
|
||||
if let peerId = strongSelf.chatLocation.peerId {
|
||||
var hasDisabledContent = false
|
||||
if "".isEmpty {
|
||||
hasDisabledContent = false
|
||||
@ -8271,9 +8287,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
|
||||
donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId])
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
|
||||
} else if case let .customChatContents(customChatContents) = strongSelf.subject {
|
||||
customChatContents.enqueueMessages(messages: messages)
|
||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
|
||||
}
|
||||
|
||||
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in
|
||||
@ -9066,7 +9085,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId))
|
||||
let sourceMessage: Signal<EngineMessage?, NoError>
|
||||
if case let .customChatContents(customChatContents) = strongSelf.subject {
|
||||
sourceMessage = customChatContents.messages
|
||||
|> take(1)
|
||||
|> map { messages -> EngineMessage? in
|
||||
return messages.first(where: { $0.id == editMessage.messageId }).flatMap(EngineMessage.init)
|
||||
}
|
||||
} else {
|
||||
sourceMessage = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId))
|
||||
}
|
||||
|
||||
let _ = (sourceMessage
|
||||
|> deliverOnMainQueue).start(next: { [weak strongSelf] message in
|
||||
guard let strongSelf, let message else {
|
||||
return
|
||||
@ -9161,27 +9191,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
media = .keep
|
||||
}
|
||||
|
||||
let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId)
|
||||
|> deliverOnMainQueue)
|
||||
.startStandalone(next: { [weak self] currentMessage in
|
||||
if let strongSelf = self {
|
||||
if let currentMessage = currentMessage {
|
||||
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? []
|
||||
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false)
|
||||
|
||||
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview {
|
||||
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
|
||||
if case let .customChatContents(customChatContents) = strongSelf.subject {
|
||||
customChatContents.editMessage(id: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||||
var state = state
|
||||
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
|
||||
state = state.updatedEditMessageState(nil)
|
||||
return state
|
||||
})
|
||||
} else {
|
||||
let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] currentMessage in
|
||||
if let strongSelf = self {
|
||||
if let currentMessage = currentMessage {
|
||||
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? []
|
||||
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false)
|
||||
|
||||
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview {
|
||||
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||||
var state = state
|
||||
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
|
||||
state = state.updatedEditMessageState(nil)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||||
var state = state
|
||||
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
|
||||
state = state.updatedEditMessageState(nil)
|
||||
return state
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}, beginMessageSearch: { [weak self] domain, query in
|
||||
guard let strongSelf = self else {
|
||||
@ -9313,7 +9353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||||
if let navigateIndex = navigateIndex {
|
||||
switch strongSelf.chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
|
||||
}
|
||||
}
|
||||
@ -11249,7 +11289,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
|
||||
case let .replyThread(replyThreadMessage):
|
||||
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
activitySpace = nil
|
||||
}
|
||||
|
||||
@ -12152,7 +12192,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
peerId = replyThreadMessage.peerId
|
||||
threadId = replyThreadMessage.threadId
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return
|
||||
}
|
||||
|
||||
@ -12710,7 +12750,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.effectiveNavigationController?.pushViewController(infoController)
|
||||
}
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
})
|
||||
@ -12922,7 +12962,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}))
|
||||
case .replyThread:
|
||||
break
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -13654,7 +13694,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let effectiveMessageId = replyThreadMessage.effectiveMessageId {
|
||||
defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil)
|
||||
}
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
|
||||
@ -13709,6 +13749,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) {
|
||||
if case let .customChatContents(customChatContents) = self.subject {
|
||||
customChatContents.enqueueMessages(messages: messages)
|
||||
return
|
||||
}
|
||||
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
|
@ -588,6 +588,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, loadMore: nil)
|
||||
}
|
||||
} else if case .customChatContents = chatLocation {
|
||||
if case let .customChatContents(customChatContents) = subject {
|
||||
source = .custom(
|
||||
messages: customChatContents.messages
|
||||
|> map { messages in
|
||||
return (messages, 0, false)
|
||||
},
|
||||
messageId: nil,
|
||||
quote: nil,
|
||||
loadMore: nil
|
||||
)
|
||||
} else {
|
||||
source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, loadMore: nil)
|
||||
}
|
||||
} else {
|
||||
source = .default
|
||||
}
|
||||
|
@ -39,10 +39,6 @@ extension ChatControllerImpl {
|
||||
}
|
||||
|
||||
func presentAttachmentMenu(subject: AttachMenuSubject) {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let inputIsActive = self.presentationInterfaceState.inputMode == .text
|
||||
|
||||
@ -56,42 +52,46 @@ extension ChatControllerImpl {
|
||||
var bannedSendFiles: (Int32, Bool)?
|
||||
|
||||
var canSendPolls = true
|
||||
if let peer = peer as? TelegramUser, peer.botInfo == nil {
|
||||
canSendPolls = false
|
||||
} else if peer is TelegramSecretChat {
|
||||
canSendPolls = false
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) {
|
||||
bannedSendPhotos = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendVideos, ignoreDefault: canByPassRestrictions) {
|
||||
bannedSendVideos = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendFiles, ignoreDefault: canByPassRestrictions) {
|
||||
bannedSendFiles = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) {
|
||||
banSendText = value
|
||||
}
|
||||
if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil {
|
||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
||||
if let peer = peer as? TelegramUser, peer.botInfo == nil {
|
||||
canSendPolls = false
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if group.hasBannedPermission(.banSendPhotos) {
|
||||
bannedSendPhotos = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendVideos) {
|
||||
bannedSendVideos = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendFiles) {
|
||||
bannedSendFiles = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendText) {
|
||||
banSendText = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendPolls) {
|
||||
} else if peer is TelegramSecretChat {
|
||||
canSendPolls = false
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) {
|
||||
bannedSendPhotos = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendVideos, ignoreDefault: canByPassRestrictions) {
|
||||
bannedSendVideos = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendFiles, ignoreDefault: canByPassRestrictions) {
|
||||
bannedSendFiles = value
|
||||
}
|
||||
if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) {
|
||||
banSendText = value
|
||||
}
|
||||
if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil {
|
||||
canSendPolls = false
|
||||
}
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
if group.hasBannedPermission(.banSendPhotos) {
|
||||
bannedSendPhotos = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendVideos) {
|
||||
bannedSendVideos = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendFiles) {
|
||||
bannedSendFiles = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendText) {
|
||||
banSendText = (Int32.max, false)
|
||||
}
|
||||
if group.hasBannedPermission(.banSendPolls) {
|
||||
canSendPolls = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
canSendPolls = false
|
||||
}
|
||||
|
||||
var availableButtons: [AttachmentButtonType] = [.gallery, .file]
|
||||
@ -111,24 +111,26 @@ extension ChatControllerImpl {
|
||||
}
|
||||
|
||||
var peerType: AttachMenuBots.Bot.PeerFlags = []
|
||||
if let user = peer as? TelegramUser {
|
||||
if let _ = user.botInfo {
|
||||
peerType.insert(.bot)
|
||||
} else {
|
||||
peerType.insert(.user)
|
||||
}
|
||||
} else if let _ = peer as? TelegramGroup {
|
||||
peerType = .group
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
peerType = .channel
|
||||
} else {
|
||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
||||
if let user = peer as? TelegramUser {
|
||||
if let _ = user.botInfo {
|
||||
peerType.insert(.bot)
|
||||
} else {
|
||||
peerType.insert(.user)
|
||||
}
|
||||
} else if let _ = peer as? TelegramGroup {
|
||||
peerType = .group
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
peerType = .channel
|
||||
} else {
|
||||
peerType = .group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError>
|
||||
if !isScheduledMessages && !peer.isDeleted {
|
||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
|
||||
buttons = self.context.engine.messages.attachMenuBots()
|
||||
|> map { attachMenuBots in
|
||||
var buttons = availableButtons
|
||||
@ -177,7 +179,7 @@ extension ChatControllerImpl {
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
|
||||
let premiumGiftOptions: [CachedPremiumGiftOption]
|
||||
if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !premiumConfiguration.isPremiumDisabled, premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) {
|
||||
premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions
|
||||
} else {
|
||||
premiumGiftOptions = []
|
||||
@ -324,20 +326,30 @@ extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
let selfPeerId: PeerId
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
selfPeerId = peer.id
|
||||
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
|
||||
selfPeerId = peer.id
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||
selfPeerId = peer.id
|
||||
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
|
||||
selfPeerId = peer.id
|
||||
} else {
|
||||
selfPeerId = strongSelf.context.account.peerId
|
||||
}
|
||||
} else {
|
||||
selfPeerId = strongSelf.context.account.peerId
|
||||
}
|
||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in
|
||||
|> deliverOnMainQueue).startStandalone(next: { selfPeer in
|
||||
guard let strongSelf = self, let selfPeer = selfPeer else {
|
||||
return
|
||||
}
|
||||
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
|
||||
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in
|
||||
let hasLiveLocation: Bool
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||
hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
|
||||
} else {
|
||||
hasLiveLocation = false
|
||||
}
|
||||
let sharePeer = (strongSelf.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init)
|
||||
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: sharePeer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { location, _, _, _, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -523,69 +535,73 @@ extension ChatControllerImpl {
|
||||
completion(controller, controller?.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
case .gift:
|
||||
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
|
||||
if !premiumGiftOptions.isEmpty {
|
||||
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
|
||||
if !premiumGiftOptions.isEmpty {
|
||||
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
|
||||
if let strongSelf = self {
|
||||
strongSelf.push(c)
|
||||
}
|
||||
}, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.hintPlayNextOutgoingGift()
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
}
|
||||
})
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone()
|
||||
}
|
||||
}
|
||||
case let .app(bot):
|
||||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||
var payload: String?
|
||||
var fromAttachMenu = true
|
||||
if case let .bot(_, botPayload, _) = subject {
|
||||
payload = botPayload
|
||||
fromAttachMenu = false
|
||||
}
|
||||
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId)
|
||||
controller.openUrl = { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}
|
||||
controller.getNavigationController = { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
}
|
||||
controller.completion = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.push(c)
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||
})
|
||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
}, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.hintPlayNextOutgoingGift()
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone()
|
||||
}
|
||||
case let .app(bot):
|
||||
var payload: String?
|
||||
var fromAttachMenu = true
|
||||
if case let .bot(_, botPayload, _) = subject {
|
||||
payload = botPayload
|
||||
fromAttachMenu = false
|
||||
}
|
||||
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId)
|
||||
controller.openUrl = { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}
|
||||
controller.getNavigationController = { [weak self] in
|
||||
return self?.effectiveNavigationController
|
||||
}
|
||||
controller.completion = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||
if bot.flags.contains(.notActivated) {
|
||||
let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if bot.flags.contains(.showInSettingsDisclaimer) {
|
||||
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
|
||||
}
|
||||
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
}, completed: { [weak controller] in
|
||||
controller?.refresh()
|
||||
})
|
||||
},
|
||||
dismissed: {
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
})
|
||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
strongSelf.present(alertController, in: .window(.root))
|
||||
}
|
||||
}
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set(nil)
|
||||
|
||||
if bot.flags.contains(.notActivated) {
|
||||
let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if bot.flags.contains(.showInSettingsDisclaimer) {
|
||||
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
|
||||
}
|
||||
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
}, completed: { [weak controller] in
|
||||
controller?.refresh()
|
||||
})
|
||||
},
|
||||
dismissed: {
|
||||
strongSelf.attachmentController?.dismiss(animated: true)
|
||||
})
|
||||
strongSelf.present(alertController, in: .window(.root))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1063,9 +1079,6 @@ extension ChatControllerImpl {
|
||||
}
|
||||
|
||||
func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
var isScheduledMessages = false
|
||||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||||
isScheduledMessages = true
|
||||
@ -1073,7 +1086,7 @@ extension ChatControllerImpl {
|
||||
let controller = MediaPickerScreen(
|
||||
context: self.context,
|
||||
updatedPresentationData: self.updatedPresentationData,
|
||||
peer: EnginePeer(peer),
|
||||
peer: (self.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init),
|
||||
threadTitle: self.threadInfo?.title,
|
||||
chatLocation: self.chatLocation,
|
||||
isScheduledMessages: isScheduledMessages,
|
||||
|
@ -59,7 +59,7 @@ extension ChatControllerImpl {
|
||||
case let .replyThread(replyThreadMessage):
|
||||
peerId = replyThreadMessage.peerId
|
||||
threadId = replyThreadMessage.threadId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ extension ChatControllerImpl {
|
||||
case let .replyThread(replyThreadMessage):
|
||||
peerId = replyThreadMessage.peerId
|
||||
threadId = replyThreadMessage.threadId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ extension ChatControllerImpl {
|
||||
break
|
||||
case let .replyThread(replyThreadMessage):
|
||||
threadId = replyThreadMessage.threadId
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ extension ChatControllerImpl {
|
||||
})
|
||||
if let navigateIndex = navigateIndex {
|
||||
switch strongSelf.chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
|
||||
text = interfaceState.strings.ChatList_StartMessaging
|
||||
} else {
|
||||
switch interfaceState.chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
if case .scheduledMessages = interfaceState.subject {
|
||||
text = interfaceState.strings.ScheduledMessages_EmptyPlaceholder
|
||||
} else {
|
||||
@ -701,6 +701,12 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
|
||||
}
|
||||
|
||||
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var maxWidth: CGFloat = size.width
|
||||
var centerText = false
|
||||
if case .customChatContents = interfaceState.subject {
|
||||
maxWidth = min(240.0, maxWidth)
|
||||
}
|
||||
|
||||
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
|
||||
self.currentTheme = interfaceState.theme
|
||||
self.currentStrings = interfaceState.strings
|
||||
@ -709,17 +715,56 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
|
||||
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: serviceColor.primaryText)
|
||||
|
||||
let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title
|
||||
let titleString: String
|
||||
let strings: [String]
|
||||
|
||||
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput:
|
||||
//TODO:localize
|
||||
centerText = true
|
||||
titleString = "New Greeting Message"
|
||||
strings = [
|
||||
"Create greetings that will be automatically sent to new customers"
|
||||
]
|
||||
case .awayMessageInput:
|
||||
//TODO:localize
|
||||
centerText = true
|
||||
titleString = "New Away Message"
|
||||
strings = [
|
||||
"Add messages that are automatically sent when you are off."
|
||||
]
|
||||
case let .quickReplyMessageInput(shortcut):
|
||||
//TODO:localize
|
||||
centerText = false
|
||||
titleString = "New Quick Reply"
|
||||
strings = [
|
||||
"Enter a message below that will be sent in chats when you type \"**/\(shortcut)\"**.",
|
||||
"You can access Quick Replies in any chat by typing \"/\" or using the Attachment menu."
|
||||
]
|
||||
}
|
||||
} else {
|
||||
titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title
|
||||
strings = [
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description1,
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description2,
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description3,
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description4
|
||||
]
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText)
|
||||
|
||||
let strings: [String] = [
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description1,
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description2,
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description3,
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description4
|
||||
]
|
||||
|
||||
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) }
|
||||
let lines: [NSAttributedString] = strings.map {
|
||||
return parseMarkdownIntoAttributedString($0, attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: serviceColor.primaryText),
|
||||
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText),
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
}
|
||||
), textAlignment: centerText ? .center : .natural)
|
||||
}
|
||||
|
||||
for i in 0 ..< lines.count {
|
||||
if i >= self.lineNodes.count {
|
||||
@ -727,6 +772,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
|
||||
textNode.maximumNumberOfLines = 0
|
||||
textNode.isUserInteractionEnabled = false
|
||||
textNode.displaysAsynchronously = false
|
||||
textNode.textAlignment = centerText ? .center : .natural
|
||||
self.addSubnode(textNode)
|
||||
self.lineNodes.append(textNode)
|
||||
}
|
||||
@ -751,7 +797,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
|
||||
|
||||
var lineNodes: [(CGSize, ImmediateTextNode)] = []
|
||||
for textNode in self.lineNodes {
|
||||
let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let textSize = textNode.updateLayout(CGSize(width: maxWidth - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
contentWidth = max(contentWidth, textSize.width)
|
||||
contentHeight += textSize.height + titleSpacing
|
||||
lineNodes.append((textSize, textNode))
|
||||
@ -1166,7 +1212,9 @@ final class ChatEmptyNode: ASDisplayNode {
|
||||
case .detailsPlaceholder:
|
||||
contentType = .regular
|
||||
case let .emptyChat(emptyType):
|
||||
if case .replyThread = interfaceState.chatLocation {
|
||||
if case .customChatContents = interfaceState.subject {
|
||||
contentType = .cloud
|
||||
} else if case .replyThread = interfaceState.chatLocation {
|
||||
if case .topic = emptyType {
|
||||
contentType = .topic
|
||||
} else {
|
||||
|
@ -212,13 +212,18 @@ extension ListMessageItemInteraction {
|
||||
}
|
||||
|
||||
private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] {
|
||||
var disableFloatingDateHeaders = false
|
||||
if case .customChatContents = chatLocation {
|
||||
disableFloatingDateHeaders = true
|
||||
}
|
||||
|
||||
return entries.map { entry -> ListViewInsertItem in
|
||||
switch entry.entry {
|
||||
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
|
||||
let item: ListViewItem
|
||||
switch mode {
|
||||
case .bubbles:
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
|
||||
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
||||
let displayHeader: Bool
|
||||
switch displayHeaders {
|
||||
@ -236,7 +241,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
|
||||
let item: ListViewItem
|
||||
switch mode {
|
||||
case .bubbles:
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages))
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
|
||||
case .list:
|
||||
assertionFailure()
|
||||
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
|
||||
@ -257,13 +262,18 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
|
||||
}
|
||||
|
||||
private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
|
||||
var disableFloatingDateHeaders = false
|
||||
if case .customChatContents = chatLocation {
|
||||
disableFloatingDateHeaders = true
|
||||
}
|
||||
|
||||
return entries.map { entry -> ListViewUpdateItem in
|
||||
switch entry.entry {
|
||||
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
|
||||
let item: ListViewItem
|
||||
switch mode {
|
||||
case .bubbles:
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
|
||||
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
||||
let displayHeader: Bool
|
||||
switch displayHeaders {
|
||||
@ -281,7 +291,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
|
||||
let item: ListViewItem
|
||||
switch mode {
|
||||
case .bubbles:
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages))
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
|
||||
case .list:
|
||||
assertionFailure()
|
||||
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
|
||||
@ -2011,10 +2021,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
if apply {
|
||||
switch strongSelf.chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread:
|
||||
if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
|
||||
strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex)
|
||||
}
|
||||
case .customChatContents:
|
||||
break
|
||||
}
|
||||
}
|
||||
}).strict())
|
||||
@ -2757,7 +2769,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
switch self.chatLocation {
|
||||
case .peer:
|
||||
messageIndex = maxIncomingIndex
|
||||
case .replyThread, .feed:
|
||||
case .replyThread, .customChatContents:
|
||||
messageIndex = maxOverallIndex
|
||||
}
|
||||
|
||||
@ -3142,7 +3154,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
|
||||
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
|
||||
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty)
|
||||
|
||||
var hasReachedLimits = false
|
||||
if case let .customChatContents(customChatContents) = self.subject, let messageLimit = customChatContents.messageLimit {
|
||||
hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit
|
||||
}
|
||||
|
||||
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits)
|
||||
if self.currentHistoryState != historyState {
|
||||
self.currentHistoryState = historyState
|
||||
self.historyState.set(historyState)
|
||||
@ -3403,7 +3421,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
strongSelf.historyView = transition.historyView
|
||||
|
||||
let loadState: ChatHistoryNodeLoadState
|
||||
var alwaysHasMessages = false
|
||||
if case .custom = strongSelf.source {
|
||||
if case .customChatContents = strongSelf.chatLocation {
|
||||
} else {
|
||||
alwaysHasMessages = true
|
||||
}
|
||||
}
|
||||
if alwaysHasMessages {
|
||||
loadState = .messages
|
||||
} else if let historyView = strongSelf.historyView {
|
||||
if historyView.filteredEntries.isEmpty {
|
||||
@ -3513,7 +3538,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
switch strongSelf.chatLocation {
|
||||
case .peer:
|
||||
messageIndex = incomingIndex
|
||||
case .replyThread, .feed:
|
||||
case .replyThread, .customChatContents:
|
||||
messageIndex = overallIndex
|
||||
}
|
||||
|
||||
@ -3534,7 +3559,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
|
||||
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
|
||||
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty)
|
||||
var hasReachedLimits = false
|
||||
if case let .customChatContents(customChatContents) = strongSelf.subject, let messageLimit = customChatContents.messageLimit {
|
||||
hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit
|
||||
}
|
||||
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits)
|
||||
if strongSelf.currentHistoryState != historyState {
|
||||
strongSelf.currentHistoryState = historyState
|
||||
strongSelf.historyState.set(historyState)
|
||||
@ -4027,6 +4056,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
|
||||
if let messageItem = messageItem {
|
||||
let associatedData = messageItem.associatedData
|
||||
let disableFloatingDateHeaders = messageItem.disableDate
|
||||
|
||||
loop: for i in 0 ..< historyView.filteredEntries.count {
|
||||
switch historyView.filteredEntries[i] {
|
||||
@ -4036,7 +4066,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
let item: ListViewItem
|
||||
switch self.mode {
|
||||
case .bubbles:
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
|
||||
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
||||
let displayHeader: Bool
|
||||
switch displayHeaders {
|
||||
@ -4083,6 +4113,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
|
||||
if let messageItem = messageItem {
|
||||
let associatedData = messageItem.associatedData
|
||||
let disableFloatingDateHeaders = messageItem.disableDate
|
||||
|
||||
loop: for i in 0 ..< historyView.filteredEntries.count {
|
||||
switch historyView.filteredEntries[i] {
|
||||
@ -4092,7 +4123,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
let item: ListViewItem
|
||||
switch self.mode {
|
||||
case .bubbles:
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
|
||||
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
|
||||
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
||||
let displayHeader: Bool
|
||||
switch displayHeaders {
|
||||
|
@ -333,7 +333,7 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL
|
||||
readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalState: totalUnreadState, notificationSettings: notificationSettings)
|
||||
}
|
||||
}
|
||||
case .replyThread, .feed:
|
||||
case .replyThread, .customChatContents:
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
@ -337,7 +337,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
|
||||
}
|
||||
case .replyThread:
|
||||
canReply = true
|
||||
case .feed:
|
||||
case .customChatContents:
|
||||
canReply = false
|
||||
}
|
||||
return canReply
|
||||
@ -597,6 +597,68 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
return .single(ContextController.Items(content: .list(actions)))
|
||||
}
|
||||
|
||||
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
|
||||
var actions: [ContextMenuItem] = []
|
||||
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
var restrictedText: String?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
messageEntities = attribute.entities
|
||||
}
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
if let restrictedText = restrictedText {
|
||||
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
||||
} else {
|
||||
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
|
||||
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
|
||||
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
|
||||
} else {
|
||||
storeMessageTextInPasteboard(message.text, entities: messageEntities)
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
|
||||
controllerInteraction.displayUndo(content)
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { c, f in
|
||||
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
|
||||
f(.custom(transition))
|
||||
})
|
||||
})))
|
||||
|
||||
actions.append(.separator)
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { [weak customChatContents] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let customChatContents else {
|
||||
return
|
||||
}
|
||||
customChatContents.deleteMessages(ids: messages.map(\.id))
|
||||
})))
|
||||
}
|
||||
|
||||
return .single(ContextController.Items(content: .list(actions)))
|
||||
}
|
||||
|
||||
var loadStickerSaveStatus: MediaId?
|
||||
var loadCopyMediaResource: MediaResource?
|
||||
var isAction = false
|
||||
@ -654,7 +716,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
canPin = false
|
||||
} else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty {
|
||||
switch chatPresentationInterfaceState.chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
|
||||
if !isAction {
|
||||
canPin = channel.hasPermission(.pinMessages)
|
||||
|
@ -368,7 +368,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
||||
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
|
||||
displayBotStartPanel = true
|
||||
}
|
||||
} else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState {
|
||||
} else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState {
|
||||
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
|
||||
displayBotStartPanel = true
|
||||
}
|
||||
@ -401,6 +401,24 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
|
||||
}
|
||||
}
|
||||
|
||||
if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
|
||||
displayInputTextPanel = true
|
||||
}
|
||||
|
||||
if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(_, true) = chatHistoryState {
|
||||
if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) {
|
||||
return (currentPanel, nil)
|
||||
} else {
|
||||
let panel = ChatRestrictedInputPanelNode()
|
||||
panel.context = context
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
return (panel, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .inline = chatPresentationInterfaceState.mode {
|
||||
displayInputTextPanel = false
|
||||
}
|
||||
|
@ -54,6 +54,15 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .customChatContents = presentationInterfaceState.subject {
|
||||
if case .spacer = currentButton?.action {
|
||||
return currentButton
|
||||
} else {
|
||||
return ChatNavigationButton(action: .spacer, buttonItem: UIBarButtonItem(title: " ", style: .plain, target: nil, action: nil))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -95,7 +104,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
|
||||
|
||||
var hasMessages = false
|
||||
if let chatHistoryState = presentationInterfaceState.chatHistoryState {
|
||||
if case .loaded(false) = chatHistoryState {
|
||||
if case .loaded(false, _) = chatHistoryState {
|
||||
hasMessages = true
|
||||
}
|
||||
}
|
||||
@ -108,6 +117,16 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
|
||||
return nil
|
||||
}
|
||||
|
||||
if case .customChatContents = presentationInterfaceState.subject {
|
||||
if let currentButton = currentButton, currentButton.action == .dismiss {
|
||||
return currentButton
|
||||
} else {
|
||||
let buttonItem = UIBarButtonItem(title: strings.Common_Done, style: .done, target: target, action: selector)
|
||||
buttonItem.accessibilityLabel = strings.Common_Done
|
||||
return ChatNavigationButton(action: .dismiss, buttonItem: buttonItem)
|
||||
}
|
||||
}
|
||||
|
||||
if case .replyThread = presentationInterfaceState.chatLocation {
|
||||
if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||
} else if hasMessages {
|
||||
|
@ -34,6 +34,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
||||
let blurBackground: Bool = true
|
||||
let centerVertically: Bool
|
||||
|
||||
private weak var chatController: ChatControllerImpl?
|
||||
private weak var chatNode: ChatControllerNode?
|
||||
private let engine: TelegramEngine
|
||||
private let message: Message
|
||||
@ -43,6 +44,9 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
||||
if self.message.adAttribute != nil {
|
||||
return .single(false)
|
||||
}
|
||||
if let chatController = self.chatController, case .customChatContents = chatController.subject {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: self.message.id))
|
||||
|> map { message -> Bool in
|
||||
@ -55,7 +59,8 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
|
||||
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
|
||||
self.chatController = chatController
|
||||
self.chatNode = chatNode
|
||||
self.engine = engine
|
||||
self.message = message
|
||||
|
@ -101,6 +101,14 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
|
||||
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_DefaultRestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
|
||||
}
|
||||
}
|
||||
} else if case let .customChatContents(customChatContents) = interfaceState.subject {
|
||||
let displayCount: Int
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
|
||||
displayCount = 20
|
||||
}
|
||||
//TODO:localize
|
||||
self.textNode.attributedText = NSAttributedString(string: "Limit of \(displayCount) messages reached", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
|
||||
}
|
||||
self.buttonNode.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
|
||||
|
@ -34,7 +34,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
|
||||
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern)
|
||||
let placeholderText: String
|
||||
switch chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags {
|
||||
if case .standard(.embedded(false)) = presentationInterfaceState.mode {
|
||||
placeholderText = strings.Common_Search
|
||||
@ -114,7 +114,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
|
||||
self.searchBar.prefixString = nil
|
||||
let placeholderText: String
|
||||
switch self.chatLocation {
|
||||
case .peer, .replyThread, .feed:
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
if presentationInterfaceState.historyFilter != nil {
|
||||
placeholderText = self.strings.Common_Search
|
||||
} else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags {
|
||||
|
@ -1517,7 +1517,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
|
||||
} else {
|
||||
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
|
||||
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true) = chatHistoryState {
|
||||
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState {
|
||||
displayBotStartButton = true
|
||||
} else if interfaceState.peerIsBlocked {
|
||||
displayBotStartButton = true
|
||||
@ -1801,38 +1801,56 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
||||
let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId
|
||||
let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject
|
||||
|
||||
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id {
|
||||
var peerUpdated = false
|
||||
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) {
|
||||
peerUpdated = true
|
||||
}
|
||||
|
||||
if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id {
|
||||
self.initializedPlaceholder = true
|
||||
|
||||
var placeholder: String
|
||||
var placeholder: String = ""
|
||||
|
||||
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
if interfaceState.interfaceState.silentPosting {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
|
||||
}
|
||||
} else {
|
||||
if sendingTextDisabled {
|
||||
placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed
|
||||
} else {
|
||||
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
|
||||
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId {
|
||||
if replyThreadMessage.isChannelPost {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData {
|
||||
if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo {
|
||||
placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string
|
||||
}
|
||||
if let peer = interfaceState.renderedPeer?.peer {
|
||||
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
if interfaceState.interfaceState.silentPosting {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
|
||||
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
|
||||
}
|
||||
} else {
|
||||
if sendingTextDisabled {
|
||||
placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed
|
||||
} else {
|
||||
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
|
||||
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId {
|
||||
if replyThreadMessage.isChannelPost {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData {
|
||||
if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo {
|
||||
placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string
|
||||
}
|
||||
} else {
|
||||
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if case let .customChatContents(customChatContents) = interfaceState.subject {
|
||||
//TODO:localize
|
||||
switch customChatContents.kind {
|
||||
case .greetingMessageInput:
|
||||
placeholder = "Add greeting message..."
|
||||
case .awayMessageInput:
|
||||
placeholder = "Add away message..."
|
||||
case .quickReplyMessageInput:
|
||||
placeholder = "Add quick reply..."
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1899,8 +1899,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return BusinessHoursSetupScreen(context: context)
|
||||
}
|
||||
|
||||
public func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController {
|
||||
return GreetingMessageSetupScreen(context: context)
|
||||
public func makeGreetingMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController {
|
||||
return GreetingMessageSetupScreen(context: context, mode: isAwayMode ? .away : .greeting)
|
||||
}
|
||||
|
||||
public func makeQuickReplySetupScreen(context: AccountContext) -> ViewController {
|
||||
return QuickReplySetupScreen(context: context)
|
||||
}
|
||||
|
||||
public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController {
|
||||
|
Loading…
x
Reference in New Issue
Block a user