[WIP] Business

This commit is contained in:
Isaac 2024-02-16 22:52:01 +04:00
parent 16c226c801
commit 46881c65ca
69 changed files with 4103 additions and 1236 deletions
submodules
AccountContext/Sources
ChatListUI/Sources/Node
ChatPresentationInterfaceState/Sources
GalleryUI/Sources
ItemListPeerActionItem/Sources
Postbox/Sources
SearchPeerMembers/Sources
TelegramCore/Sources/State
TelegramStringFormatting/Sources
TelegramUI
Components
Chat
ChatMessageAnimatedStickerItemNode/Sources
ChatMessageAttachedContentNode/Sources
ChatMessageBubbleItemNode/Sources
ChatMessageContactBubbleContentNode/Sources
ChatMessageFileBubbleContentNode/Sources
ChatMessageInstantVideoBubbleContentNode/Sources
ChatMessageInstantVideoItemNode/Sources
ChatMessageInteractiveInstantVideoNode/Sources
ChatMessageItemImpl/Sources
ChatMessageMapBubbleContentNode/Sources
ChatMessageMediaBubbleContentNode/Sources
ChatMessagePollBubbleContentNode/Sources
ChatMessageRestrictedBubbleContentNode/Sources
ChatMessageStickerItemNode/Sources
ChatMessageTextBubbleContentNode/Sources
ListItemSliderSelectorComponent
Settings
SliderComponent
TimeSelectionActionSheet
Resources/Animations
Sources

@ -395,13 +395,13 @@ public enum ChatSearchDomain: Equatable {
public enum ChatLocation: Equatable { public enum ChatLocation: Equatable {
case peer(id: PeerId) case peer(id: PeerId)
case replyThread(message: ChatReplyThreadMessage) case replyThread(message: ChatReplyThreadMessage)
case feed(id: Int32) case customChatContents
} }
public extension ChatLocation { public extension ChatLocation {
var normalized: ChatLocation { var normalized: ChatLocation {
switch self { switch self {
case .peer, .feed: case .peer, .customChatContents:
return self return self
case let .replyThread(message): case let .replyThread(message):
return .replyThread(message: message.normalized) return .replyThread(message: message.normalized)
@ -936,7 +936,8 @@ public protocol SharedAccountContext: AnyObject {
func makeChatbotSetupScreen(context: AccountContext) -> ViewController func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController
func makeBusinessHoursSetupScreen(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 navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError> 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 scheduledMessages
case pinnedMessages(id: EngineMessage.Id?) case pinnedMessages(id: EngineMessage.Id?)
case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo) case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo)
case customChatContents(contents: ChatCustomContentsProtocol)
public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool { public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool {
switch lhs { switch lhs {
@ -771,6 +772,12 @@ public enum ChatControllerSubject: Equatable {
} else { } else {
return false 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 `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 { public enum ChatHistoryListDisplayHeaders {
@ -1069,7 +1092,7 @@ public protocol ChatControllerInteractionProtocol: AnyObject {
public enum ChatHistoryNodeHistoryState: Equatable { public enum ChatHistoryNodeHistoryState: Equatable {
case loading case loading
case loaded(isEmpty: Bool) case loaded(isEmpty: Bool, hasReachedLimits: Bool)
} }
public protocol ChatHistoryListNode: ListView { public protocol ChatHistoryListNode: ListView {

@ -36,8 +36,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
return .peer(peerId) return .peer(peerId)
case let .replyThread(replyThreaMessage): case let .replyThread(replyThreaMessage):
return .peer(replyThreaMessage.peerId) return .peer(replyThreaMessage.peerId)
case let .feed(id): case .customChatContents:
return .feed(id) return .custom
} }
case let .singleMessage(id): case let .singleMessage(id):
return .peer(id.peerId) return .peer(id.peerId)

@ -26,6 +26,7 @@ import TextNodeWithEntities
import ComponentFlow import ComponentFlow
import EmojiStatusComponent import EmojiStatusComponent
import AvatarVideoNode import AvatarVideoNode
import AppBundle
public enum ChatListItemContent { public enum ChatListItemContent {
public struct ThreadInfo: Equatable { 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 struct PeerData {
public var messages: [EngineMessage] public var messages: [EngineMessage]
public var peer: EngineRenderedPeer public var peer: EngineRenderedPeer
@ -112,6 +123,7 @@ public enum ChatListItemContent {
public var requiresPremiumForMessaging: Bool public var requiresPremiumForMessaging: Bool
public var displayAsTopicList: Bool public var displayAsTopicList: Bool
public var tags: [Tag] public var tags: [Tag]
public var customMessageListData: CustomMessageListData?
public init( public init(
messages: [EngineMessage], messages: [EngineMessage],
@ -135,7 +147,8 @@ public enum ChatListItemContent {
storyState: StoryState?, storyState: StoryState?,
requiresPremiumForMessaging: Bool, requiresPremiumForMessaging: Bool,
displayAsTopicList: Bool, displayAsTopicList: Bool,
tags: [Tag] tags: [Tag],
customMessageListData: CustomMessageListData? = nil
) { ) {
self.messages = messages self.messages = messages
self.peer = peer self.peer = peer
@ -159,6 +172,7 @@ public enum ChatListItemContent {
self.requiresPremiumForMessaging = requiresPremiumForMessaging self.requiresPremiumForMessaging = requiresPremiumForMessaging
self.displayAsTopicList = displayAsTopicList self.displayAsTopicList = displayAsTopicList
self.tags = tags self.tags = tags
self.customMessageListData = customMessageListData
} }
} }
@ -1153,6 +1167,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let inputActivitiesNode: ChatListInputActivitiesNode let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode let dateNode: TextNode
var dateStatusIconNode: ASImageNode? var dateStatusIconNode: ASImageNode?
var dateDisclosureIconView: UIImageView?
let separatorNode: ASDisplayNode let separatorNode: ASDisplayNode
let statusNode: ChatListStatusNode let statusNode: ChatListStatusNode
let badgeNode: ChatListBadgeNode let badgeNode: ChatListBadgeNode
@ -1587,7 +1602,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let peer = peer { if let peer = peer {
var overrideImage: AvatarNodeImageOverride? var overrideImage: AvatarNodeImageOverride?
if peer.id.isReplies { if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
} else if peer.id.isReplies {
overrideImage = .repliesIcon overrideImage = .repliesIcon
} else if peer.id.isAnonymousSavedMessages { } else if peer.id.isAnonymousSavedMessages {
overrideImage = .anonymousSavedMessagesIcon overrideImage = .anonymousSavedMessagesIcon
@ -2021,15 +2037,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let enableChatListPhotos = true 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 let avatarLeftInset: CGFloat
if item.interaction.isInlineMode {
avatarLeftInset = 12.0 if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
} else if !useChatListLayout { avatarDiameter = 40.0
avatarLeftInset = 50.0
} else {
avatarLeftInset = 18.0 + avatarDiameter 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) let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
@ -2083,7 +2105,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
hideAuthor = true hideAuthor = true
} }
let attributedText: NSAttributedString var attributedText: NSAttributedString
var hasDraft = false var hasDraft = false
var inlineAuthorPrefix: String? var inlineAuthorPrefix: String?
@ -2401,6 +2423,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
attributedText = composedString 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 !ignoreForwardedIcon {
if case .savedMessagesChats = item.chatListLocation { if case .savedMessagesChats = item.chatListLocation {
displayForwardedIcon = false displayForwardedIcon = false
@ -2548,7 +2577,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch contentData { switch contentData {
case let .chat(itemPeer, threadInfo, _, _, _, _, _): 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) titleAttributedString = NSAttributedString(string: threadInfo.info.title, font: titleFont, textColor: theme.titleColor)
} else if let message = messages.last, case let .user(author) = message.author, displayAsMessage { } 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) 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): case let .peer(peerData):
topIndex = peerData.messages.first?.index 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 t = Int(topIndex.timestamp)
var timeinfo = tm() var timeinfo = tm()
localtime_r(&t, &timeinfo) localtime_r(&t, &timeinfo)
@ -2754,7 +2803,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content { switch item.content {
case let .peer(peerData): case let .peer(peerData):
if let peer = peerData.messages.last?.author { 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 currentCredibilityIconContent = nil
} else if peer.isScam { } else if peer.isScam {
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) 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 break
} }
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { } 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 currentCredibilityIconContent = nil
} else if peer.isScam { } else if peer.isScam {
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) 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 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 titleSpacing: CGFloat = -1.0
let authorSpacing: CGFloat = -3.0 let authorSpacing: CGFloat = -3.0
var itemHeight: CGFloat = 8.0 * 2.0 + 1.0 var itemHeight: CGFloat = 8.0 * 2.0 + 1.0
itemHeight -= 21.0 itemHeight -= 21.0
itemHeight += titleLayout.size.height if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
itemHeight += measureLayout.size.height * 3.0 itemHeight += measureLayout.size.height * 2.0
itemHeight += titleSpacing itemHeight += 22.0
itemHeight += authorSpacing } 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)) 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 _ = mentionBadgeApply(animateBadges, true)
let _ = onlineApply(animateContent && animateOnline) 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 var statusOffset: CGFloat = 0.0
if let dateIconImage { if let dateIconImage {
@ -3997,6 +4080,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0) 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))) 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 backgroundColor: UIColor
let highlightedBackgroundColor: UIColor let highlightedBackgroundColor: UIColor
@ -4012,7 +4101,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor
} }
} else { } else {
backgroundColor = theme.itemBackgroundColor if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
backgroundColor = .clear
} else {
backgroundColor = theme.itemBackgroundColor
}
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
} }

@ -17,7 +17,7 @@ public extension ChatLocation {
return peerId return peerId
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
return replyThreadMessage.peerId return replyThreadMessage.peerId
case .feed: case .customChatContents:
return nil return nil
} }
} }
@ -28,7 +28,7 @@ public extension ChatLocation {
return nil return nil
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
return replyThreadMessage.threadId return replyThreadMessage.threadId
case .feed: case .customChatContents:
return nil return nil
} }
} }

@ -605,7 +605,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
case let .replyThread(message): case let .replyThread(message):
peerIdValue = message.peerId peerIdValue = message.peerId
threadIdValue = message.threadId threadIdValue = message.threadId
case .feed: case .customChatContents:
break break
} }
if peerIdValue == context.account.peerId, let customTag { if peerIdValue == context.account.peerId, let customTag {

@ -55,6 +55,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
if self.alwaysPlain { if self.alwaysPlain {
neighbors.top = .sameSection(alwaysPlain: false) neighbors.top = .sameSection(alwaysPlain: false)
} }
if self.alwaysPlain {
neighbors.bottom = .sameSection(alwaysPlain: false)
}
let (layout, apply) = node.asyncLayout()(self, params, neighbors) let (layout, apply) = node.asyncLayout()(self, params, neighbors)
node.contentSize = layout.contentSize node.contentSize = layout.contentSize
@ -83,6 +86,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
if self.alwaysPlain { if self.alwaysPlain {
neighbors.top = .sameSection(alwaysPlain: false) neighbors.top = .sameSection(alwaysPlain: false)
} }
if self.alwaysPlain {
neighbors.bottom = .sameSection(alwaysPlain: false)
}
let (layout, apply) = makeLayout(self, params, neighbors) let (layout, apply) = makeLayout(self, params, neighbors)
Queue.mainQueue().async { Queue.mainQueue().async {
completion(layout, { _ in completion(layout, { _ in

@ -4,7 +4,7 @@ import SwiftSignalKit
public enum ChatLocationInput { public enum ChatLocationInput {
case peer(peerId: PeerId, threadId: Int64?) case peer(peerId: PeerId, threadId: Int64?)
case thread(peerId: PeerId, threadId: Int64, data: Signal<MessageHistoryViewExternalInput, NoError>) case thread(peerId: PeerId, threadId: Int64, data: Signal<MessageHistoryViewExternalInput, NoError>)
case feed(id: Int32, data: Signal<MessageHistoryViewExternalInput, NoError>) case customChatContents
} }
public extension ChatLocationInput { public extension ChatLocationInput {
@ -14,7 +14,7 @@ public extension ChatLocationInput {
return peerId return peerId
case let .thread(peerId, _, _): case let .thread(peerId, _, _):
return peerId return peerId
case .feed: case .customChatContents:
return nil return nil
} }
} }
@ -25,7 +25,7 @@ public extension ChatLocationInput {
return threadId return threadId
case let .thread(_, threadId, _): case let .thread(_, threadId, _):
return threadId return threadId
case .feed: case .customChatContents:
return nil return nil
} }
} }

@ -639,6 +639,10 @@ public extension MessageAttribute {
public struct MessageGroupInfo: Equatable { public struct MessageGroupInfo: Equatable {
public let stableId: UInt32 public let stableId: UInt32
public init(stableId: UInt32) {
self.stableId = stableId
}
} }
public final class Message { public final class Message {

@ -1101,7 +1101,7 @@ public final class MessageHistoryView {
self.topTaggedMessages = [] self.topTaggedMessages = []
self.additionalData = [] self.additionalData = []
self.isLoading = isLoading self.isLoading = isLoading
self.isLoadingEarlier = true self.isLoadingEarlier = false
self.isAddedToChatList = false self.isAddedToChatList = false
self.peerStoryStats = [:] self.peerStoryStats = [:]
} }

@ -2997,6 +2997,8 @@ final class PostboxImpl {
private func internalTransaction<T>(_ f: (Transaction) -> T) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) { private func internalTransaction<T>(_ f: (Transaction) -> T) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) {
let _ = self.isInTransaction.swap(true) let _ = self.isInTransaction.swap(true)
let startTime = CFAbsoluteTimeGetCurrent()
self.valueBox.begin() self.valueBox.begin()
let transaction = Transaction(queue: self.queue, postbox: self) let transaction = Transaction(queue: self.queue, postbox: self)
self.afterBegin(transaction: transaction) self.afterBegin(transaction: transaction)
@ -3005,6 +3007,12 @@ final class PostboxImpl {
transaction.disposed = true transaction.disposed = true
self.valueBox.commit() 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) let _ = self.isInTransaction.swap(false)
if let currentUpdatedState = self.currentUpdatedState { if let currentUpdatedState = self.currentUpdatedState {
@ -3079,7 +3087,7 @@ final class PostboxImpl {
switch chatLocation { switch chatLocation {
case let .peer(peerId, threadId): case let .peer(peerId, threadId):
return .single((.peer(peerId: peerId, threadId: threadId), false)) return .single((.peer(peerId: peerId, threadId: threadId), false))
case .thread(_, _, let data), .feed(_, let data): case .thread(_, _, let data):
return Signal { subscriber in return Signal { subscriber in
var isHoleFill = false var isHoleFill = false
return (data return (data
@ -3089,6 +3097,9 @@ final class PostboxImpl {
return (.external(value), wasHoleFill) return (.external(value), wasHoleFill)
}).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) }).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 { return ActionDisposable {
disposable.dispose() disposable.dispose()
} }
case .feed: case .customChatContents:
subscriber.putNext(([], true)) subscriber.putNext(([], true))
return ActionDisposable { return ActionDisposable {

@ -179,7 +179,7 @@ private func wrappedHistoryViewAdditionalData(chatLocation: ChatLocationInput, a
result.append(.peerChatState(peerId)) result.append(.peerChatState(peerId))
} }
} }
case .feed: case .customChatContents:
break break
} }
return result return result
@ -1839,7 +1839,7 @@ public final class AccountViewTracker {
if peerId.namespace == Namespaces.Peer.CloudChannel { if peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation) strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation)
} }
case .feed: case .customChatContents:
break break
} }
} }
@ -1852,7 +1852,7 @@ public final class AccountViewTracker {
peerId = peerIdValue peerId = peerIdValue
case let .thread(peerIdValue, _, _): case let .thread(peerIdValue, _, _):
peerId = peerIdValue peerId = peerIdValue
case .feed: case .customChatContents:
peerId = nil peerId = nil
} }
if let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel { 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 { public enum RelativeTimestampFormatDay {
case today case today
case yesterday 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 { public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
let difference = timestamp - relativeTimestamp let difference = timestamp - relativeTimestamp
if difference < 60 { if difference < 60 {

@ -818,8 +818,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if !isBroadcastChannel { if !isBroadcastChannel {
hasAvatar = true hasAvatar = true
} else if case .feed = item.chatLocation {
hasAvatar = true
} }
} }
} else if incoming { } else if incoming {
@ -844,8 +842,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else if incoming { } else if incoming {
hasAvatar = true hasAvatar = true
} }
case .feed: case .customChatContents:
hasAvatar = true hasAvatar = false
} }
if hasAvatar { 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) 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) animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
dateAndStatusApply(animation) dateAndStatusApply(animation)
if case .customChatContents = item.associatedData.subject {
strongSelf.dateAndStatusNode.isHidden = true
}
if needsReplyBackground { if needsReplyBackground {
if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { 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))? 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 { switch bottom {
case .None, .Neighbour(_, .footer, _): case .None, .Neighbour(_, .footer, _):
if message.adAttribute == nil { if message.adAttribute == nil {

@ -1446,8 +1446,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if !isBroadcastChannel { if !isBroadcastChannel {
hasAvatar = incoming hasAvatar = incoming
} else if case .feed = item.chatLocation { } else if case .customChatContents = item.chatLocation {
hasAvatar = true hasAvatar = false
} }
} }
} else if incoming { } else if incoming {
@ -2072,7 +2072,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
maximumNodeWidth = size.width 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 let message = item.content.firstMessage
var edited = false 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 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? let statusType: ChatMessageDateAndStatusType?
switch position { if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming { if incoming {
statusType = .BubbleIncoming statusType = .BubbleIncoming
@ -267,6 +270,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
} }
default: default:
statusType = nil statusType = nil
}
} }
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?

@ -112,8 +112,11 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
incoming = false incoming = false
} }
let statusType: ChatMessageDateAndStatusType? let statusType: ChatMessageDateAndStatusType?
switch preparePosition { if case .customChatContents = item.associatedData.subject {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming { if incoming {
statusType = .BubbleIncoming statusType = .BubbleIncoming
} else { } else {
@ -127,6 +130,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
} }
default: default:
statusType = nil 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!) 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? let statusType: ChatMessageDateAndStatusType?
switch preparePosition { if case .customChatContents = item.associatedData.subject {
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 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!) 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 { if !isBroadcastChannel {
hasAvatar = true hasAvatar = true
} else if case .feed = item.chatLocation { } else if case .customChatContents = item.chatLocation {
hasAvatar = true hasAvatar = false
} }
} }
} else if incoming { } else if incoming {

@ -949,6 +949,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) 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 { if let videoNode = strongSelf.videoNode {
videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
if strongSelf.imageScale != imageScale { if strongSelf.imageScale != imageScale {

@ -365,7 +365,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
} }
self.avatarHeader = avatarHeader self.avatarHeader = avatarHeader
var headers: [ListViewItemHeader] = [self.dateHeader] var headers: [ListViewItemHeader] = []
if !self.disableDate {
headers.append(self.dateHeader)
}
if case .messageOptions = associatedData.subject { if case .messageOptions = associatedData.subject {
headers = [] 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 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? let statusType: ChatMessageDateAndStatusType?
switch position { if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil { if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil {
if incoming { if incoming {
@ -252,6 +255,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
} }
default: default:
statusType = nil statusType = nil
}
} }
var statusSize = CGSize() 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 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? let statusType: ChatMessageDateAndStatusType?
switch preparePosition { if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if item.message.effectivelyIncoming(item.context.account.peerId) { if item.message.effectivelyIncoming(item.context.account.peerId) {
statusType = .ImageIncoming statusType = .ImageIncoming
@ -301,6 +304,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
statusType = nil statusType = nil
default: default:
statusType = nil statusType = nil
}
} }
var isReplyThread = false 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 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? let statusType: ChatMessageDateAndStatusType?
switch position { if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming { if incoming {
statusType = .BubbleIncoming statusType = .BubbleIncoming
@ -1001,8 +1004,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} }
default: default:
statusType = nil statusType = nil
}
} }
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType { 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 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? let statusType: ChatMessageDateAndStatusType?
switch position { if case .customChatContents = item.associatedData.subject {
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 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)] let entities = [MessageTextEntity(range: 0..<rawText.count, type: .Italic)]

@ -458,8 +458,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
if !isBroadcastChannel { if !isBroadcastChannel {
hasAvatar = true hasAvatar = true
} else if case .feed = item.chatLocation { } else if case .customChatContents = item.chatLocation {
hasAvatar = true hasAvatar = false
} }
} }
} else if incoming { } else if incoming {
@ -484,8 +484,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
} else if incoming { } else if incoming {
hasAvatar = true hasAvatar = true
} }
case .feed: case .customChatContents:
hasAvatar = true hasAvatar = false
} }
if hasAvatar { if hasAvatar {
@ -975,6 +975,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
dateAndStatusApply(animation) dateAndStatusApply(animation)
if case .customChatContents = item.associatedData.subject {
strongSelf.dateAndStatusNode.isHidden = true
}
if let updatedShareButtonNode = updatedShareButtonNode { if let updatedShareButtonNode = updatedShareButtonNode {
if updatedShareButtonNode !== strongSelf.shareButtonNode { if updatedShareButtonNode !== strongSelf.shareButtonNode {

@ -263,6 +263,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
default: default:
break break
} }
if case .customChatContents = item.associatedData.subject {
displayStatus = false
}
if displayStatus { if displayStatus {
if incoming { if incoming {
statusType = .BubbleIncoming statusType = .BubbleIncoming

@ -14,7 +14,8 @@ swift_library(
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/SliderComponent, "//submodules/TelegramUI/Components/SliderComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

@ -3,379 +3,141 @@ import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
import LegacyUI
import ComponentFlow import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import ListSectionComponent
import SliderComponent import SliderComponent
final class ListItemSliderSelectorComponent: Component { public final class ListItemSliderSelectorComponent: Component {
typealias EnvironmentType = Empty public let theme: PresentationTheme
public let values: [String]
public let selectedIndex: Int
public let selectedIndexUpdated: (Int) -> Void
let title: String public init(
let value: Float theme: PresentationTheme,
let minValue: Float values: [String],
let maxValue: Float selectedIndex: Int,
let startValue: Float selectedIndexUpdated: @escaping (Int) -> Void
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
) { ) {
self.title = title self.theme = theme
self.value = value self.values = values
self.minValue = minValue self.selectedIndex = selectedIndex
self.maxValue = maxValue self.selectedIndexUpdated = selectedIndexUpdated
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
} }
static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool { public static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool {
if lhs.title != rhs.title { if lhs.theme !== rhs.theme {
return false return false
} }
if lhs.value != rhs.value { if lhs.values != rhs.values {
return false return false
} }
if lhs.minValue != rhs.minValue { if lhs.selectedIndex != rhs.selectedIndex {
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 {
return false return false
} }
return true return true
} }
final class View: UIView, UITextFieldDelegate { public final class View: UIView, ListSectionComponent.ChildView {
private let title = ComponentView<Empty>() private var titles: [ComponentView<Empty>] = []
private let value = ComponentView<Empty>() private var slider = ComponentView<Empty>()
private var sliderView: TGPhotoEditorSliderView?
private var component: ListItemSliderSelectorComponent? private var component: ListItemSliderSelectorComponent?
private weak var state: EmptyComponentState? 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) super.init(frame: frame)
} }
required init?(coder: NSCoder) { required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") 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.component = component
self.state = state self.state = state
var internalIsTrackingUpdated: ((Bool) -> Void)? let sideInset: CGFloat = 13.0
if let isTrackingUpdated = component.isTrackingUpdated { let titleSideInset: CGFloat = 20.0
internalIsTrackingUpdated = { [weak self] isTracking in let titleClippingSideInset: CGFloat = 14.0
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)
}
if component.isEnabled { let titleAreaWidth: CGFloat = availableSize.width - titleSideInset * 2.0
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: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0))) for i in 0 ..< component.values.count {
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) var titleTransition = transition
let title: ComponentView<Empty>
let titleSize = self.title.update( if self.titles.count > i {
transition: .immediate, title = self.titles[i]
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 { } else {
valueText = "" titleTransition = titleTransition.withAnimation(.none)
title = ComponentView()
self.titles.append(title)
} }
} else { let titleSize = title.update(
valueText = "" transition: .immediate,
} component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.values[i], font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
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)
}
)
),
environment: {}, environment: {},
containerSize: availableSize containerSize: CGSize(width: 100.0, height: 100.0)
) )
sizes.append(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)
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) 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 sliderSize = self.slider.update(
let contentSize = CGSize(width: availableSize.width, height: origin.y) transition: transition,
if contentSize != self.scrollView.contentSize { component: AnyComponent(SliderComponent(
self.scrollView.contentSize = contentSize 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))
self.separatorInset = 16.0
return size
return CGSize(width: availableSize.width, height: 88.0)
} }
} }
@ -383,78 +145,7 @@ final class AdjustmentsComponent: Component {
return View(frame: CGRect()) return View(frame: CGRect())
} }
public func update(view: View, availableSize: CGSize, state: State, 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)
}
}
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 {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }
} }

@ -34,6 +34,8 @@ swift_library(
"//submodules/AppBundle", "//submodules/AppBundle",
"//submodules/TelegramStringFormatting", "//submodules/TelegramStringFormatting",
"//submodules/UIKitRuntimeUtils", "//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen",
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

@ -21,6 +21,7 @@ import Markdown
import LocationUI import LocationUI
import TelegramStringFormatting import TelegramStringFormatting
import PlainButtonComponent import PlainButtonComponent
import TimeSelectionActionSheet
final class BusinessDaySetupScreenComponent: Component { final class BusinessDaySetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment

@ -20,6 +20,7 @@ import LottieComponent
import Markdown import Markdown
import LocationUI import LocationUI
import TelegramStringFormatting import TelegramStringFormatting
import TimezoneSelectionScreen
final class BusinessHoursSetupScreenComponent: Component { final class BusinessHoursSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -75,7 +76,9 @@ final class BusinessHoursSetupScreenComponent: Component {
private let subtitle = ComponentView<Empty>() private let subtitle = ComponentView<Empty>()
private let generalSection = ComponentView<Empty>() private let generalSection = ComponentView<Empty>()
private let daysSection = ComponentView<Empty>() private let daysSection = ComponentView<Empty>()
private let timezoneSection = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var isUpdating: Bool = false private var isUpdating: Bool = false
private var component: BusinessHoursSetupScreenComponent? private var component: BusinessHoursSetupScreenComponent?
@ -84,6 +87,7 @@ final class BusinessHoursSetupScreenComponent: Component {
private var showHours: Bool = false private var showHours: Bool = false
private var days: [Day] = [] private var days: [Day] = []
private var timezoneId: String
override init(frame: CGRect) { override init(frame: CGRect) {
self.scrollView = ScrollView() self.scrollView = ScrollView()
@ -98,6 +102,8 @@ final class BusinessHoursSetupScreenComponent: Component {
} }
self.scrollView.alwaysBounceVertical = true self.scrollView.alwaysBounceVertical = true
self.timezoneId = TimeZone.current.identifier
super.init(frame: frame) super.init(frame: frame)
self.scrollView.delegate = self self.scrollView.delegate = self
@ -126,7 +132,9 @@ final class BusinessHoursSetupScreenComponent: Component {
} }
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate) if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
} }
var scrolledUp = true var scrolledUp = true
@ -320,6 +328,8 @@ final class BusinessHoursSetupScreenComponent: Component {
contentHeight += generalSectionSize.height contentHeight += generalSectionSize.height
contentHeight += sectionSpacing contentHeight += sectionSpacing
var daysContentHeight: CGFloat = 0.0
var daysSectionItems: [AnyComponentWithIdentity<Empty>] = [] var daysSectionItems: [AnyComponentWithIdentity<Empty>] = []
for day in self.days { for day in self.days {
let dayIndex = daysSectionItems.count let dayIndex = daysSectionItems.count
@ -441,7 +451,7 @@ final class BusinessHoursSetupScreenComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) 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 let daysSectionView = self.daysSection.view {
if daysSectionView.superview == nil { if daysSectionView.superview == nil {
daysSectionView.layer.allowsGroupOpacity = true daysSectionView.layer.allowsGroupOpacity = true
@ -452,13 +462,80 @@ final class BusinessHoursSetupScreenComponent: Component {
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
alphaTransition.setAlpha(view: daysSectionView, alpha: self.showHours ? 1.0 : 0.0) 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 { if self.showHours {
contentHeight += daysSectionSize.height contentHeight += daysContentHeight
} }
contentHeight += bottomContentInset contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom contentHeight += environment.safeInsets.bottom
self.ignoreScrolling = true
let previousBounds = self.scrollView.bounds let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight) let contentSize = CGSize(width: availableSize.width, height: contentHeight)
@ -472,6 +549,7 @@ final class BusinessHoursSetupScreenComponent: Component {
if self.scrollView.scrollIndicatorInsets != scrollInsets { if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets self.scrollView.scrollIndicatorInsets = scrollInsets
} }
self.ignoreScrolling = false
if !previousBounds.isEmpty, !transition.animation.isImmediate { if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds let bounds = self.scrollView.bounds

@ -263,7 +263,11 @@ final class BusinessSetupScreenComponent: Component {
icon: "Settings/Menu/Photos", icon: "Settings/Menu/Photos",
title: "Quick Replies", title: "Quick Replies",
subtitle: "Set up shortcuts with rich text and media to respond to messages faster.", 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( items.append(Item(
@ -274,14 +278,18 @@ final class BusinessSetupScreenComponent: Component {
guard let self, let component = self.component, let environment = self.environment else { guard let self, let component = self.component, let environment = self.environment else {
return 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( items.append(Item(
icon: "Settings/Menu/Trending", icon: "Settings/Menu/Trending",
title: "Away Messages", title: "Away Messages",
subtitle: "Define messages that are automatically sent when you are off.", 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( items.append(Item(

@ -34,7 +34,17 @@ swift_library(
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
"//submodules/ShimmerEffect", "//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 = [
"//visibility:public", "//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 Markdown
import PeerListItemComponent import PeerListItemComponent
import AvatarNode import AvatarNode
import ListItemSliderSelectorComponent
import DateSelectionUI
import PlainButtonComponent
import TelegramStringFormatting
import TimeSelectionActionSheet
private let checkIcon: UIImage = { private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in 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 typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let mode: GreetingMessageSetupScreen.Mode
init( init(
context: AccountContext context: AccountContext,
mode: GreetingMessageSetupScreen.Mode
) { ) {
self.context = context self.context = context
self.mode = mode
} }
static func ==(lhs: GreetingMessageSetupScreenComponent, rhs: GreetingMessageSetupScreenComponent) -> Bool { static func ==(lhs: GreetingMessageSetupScreenComponent, rhs: GreetingMessageSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.mode != rhs.mode {
return false
}
return true 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 { private struct AdditionalPeerList {
enum Category: Int { enum Category: Int {
case newChats = 0 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 { final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer() private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView private let scrollView: ScrollView
@ -113,19 +114,27 @@ final class GreetingMessageSetupScreenComponent: Component {
private let icon = ComponentView<Empty>() private let icon = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>() private let subtitle = ComponentView<Empty>()
private let generalSection = 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 accessSection = ComponentView<Empty>()
private let excludedSection = 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 isUpdating: Bool = false
private var component: GreetingMessageSetupScreenComponent? private var component: GreetingMessageSetupScreenComponent?
private(set) weak var state: EmptyComponentState? private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType? private var environment: EnvironmentType?
private var chevronImage: UIImage?
private var isOn: Bool = false 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 hasAccessToAllChatsByDefault: Bool = true
private var additionalPeerList = AdditionalPeerList( private var additionalPeerList = AdditionalPeerList(
@ -135,6 +144,8 @@ final class GreetingMessageSetupScreenComponent: Component {
private var replyToMessages: Bool = true private var replyToMessages: Bool = true
private var messagesDisposable: Disposable?
override init(frame: CGRect) { override init(frame: CGRect) {
self.scrollView = ScrollView() self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsVerticalScrollIndicator = true
@ -161,6 +172,7 @@ final class GreetingMessageSetupScreenComponent: Component {
} }
deinit { deinit {
self.messagesDisposable?.dispose()
} }
func scrollToTop() { func scrollToTop() {
@ -172,7 +184,9 @@ final class GreetingMessageSetupScreenComponent: Component {
} }
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate) if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
} }
var scrolledUp = true var scrolledUp = true
@ -332,12 +346,140 @@ final class GreetingMessageSetupScreenComponent: Component {
self.environment?.controller()?.push(controller) 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 { func update(component: GreetingMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true self.isUpdating = true
defer { defer {
self.isUpdating = false 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 environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment self.environment = environment
@ -357,7 +499,7 @@ final class GreetingMessageSetupScreenComponent: Component {
let navigationTitleSize = self.navigationTitle.update( let navigationTitleSize = self.navigationTitle.update(
transition: transition, transition: transition,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "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 horizontalAlignment: .center
)), )),
environment: {}, environment: {},
@ -387,7 +529,7 @@ final class GreetingMessageSetupScreenComponent: Component {
let iconSize = self.icon.update( let iconSize = self.icon.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(LottieComponent( component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "HandWaveEmoji"), content: LottieComponent.AppBundleContent(name: component.mode == .greeting ? "HandWaveEmoji" : "ZzzEmoji"),
loop: true loop: true
)), )),
environment: {}, environment: {},
@ -405,7 +547,7 @@ final class GreetingMessageSetupScreenComponent: Component {
contentHeight += 129.0 contentHeight += 129.0
//TODO:localize //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), body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(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), link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
@ -413,12 +555,6 @@ final class GreetingMessageSetupScreenComponent: Component {
return ("URL", "") return ("URL", "")
}), textAlignment: .center }), 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 //TODO:localize
let subtitleSize = self.subtitle.update( let subtitleSize = self.subtitle.update(
@ -463,7 +599,7 @@ final class GreetingMessageSetupScreenComponent: Component {
title: AnyComponent(VStack([ title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "Send Greeting Message", string: component.mode == .greeting ? "Send Greeting Message" : "Send Away Message",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize), font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor textColor: environment.theme.list.itemPrimaryTextColor
)), )),
@ -504,6 +640,279 @@ final class GreetingMessageSetupScreenComponent: Component {
var otherSectionsHeight: CGFloat = 0.0 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 //TODO:localize
let accessSectionSize = self.accessSection.update( let accessSectionSize = self.accessSection.update(
transition: transition, transition: transition,
@ -700,7 +1109,7 @@ final class GreetingMessageSetupScreenComponent: Component {
)), )),
footer: AnyComponent(MultilineTextComponent( footer: AnyComponent(MultilineTextComponent(
text: .markdown( 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( attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), 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), 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 += excludedSectionSize.height
otherSectionsHeight += sectionSpacing otherSectionsHeight += sectionSpacing
//TODO:localize if case .greeting = component.mode {
/*let permissionsSectionSize = self.permissionsSection.update( let periodSectionSize = self.periodSection.update(
transition: transition, transition: transition,
component: AnyComponent(ListSectionComponent( component: AnyComponent(ListSectionComponent(
theme: environment.theme, theme: environment.theme,
header: AnyComponent(MultilineTextComponent( header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "BOT PERMISSIONS", string: "PERIOD OF NO ACTIVITY",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)), )),
maximumNumberOfLines: 0 footer: AnyComponent(MultilineTextComponent(
)), text: .plain(NSAttributedString(
footer: AnyComponent(MultilineTextComponent( string: "Choose how many days should pass after your last interaction with a recipient to send them the greeting in response to their message.",
text: .plain(NSAttributedString( font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
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.", textColor: environment.theme.list.freeTextColor
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), )),
textColor: environment.theme.list.freeTextColor maximumNumberOfLines: 0
)), )),
maximumNumberOfLines: 0 items: [
)), AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
items: [ theme: environment.theme,
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( values: [
theme: environment.theme, "7 days",
title: AnyComponent(VStack([ "14 days",
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( "21 days",
text: .plain(NSAttributedString( "28 days"
string: "Reply to Messages", ],
font: Font.regular(presentationData.listsFontSize.baseDisplaySize), selectedIndex: 0,
textColor: environment.theme.list.itemPrimaryTextColor selectedIndexUpdated: { [weak self] index in
)), guard let self else {
maximumNumberOfLines: 1 return
))), }
], alignment: .left, spacing: 2.0)), let _ = self
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
guard let self else {
return
} }
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 periodSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: periodSectionSize)
environment: {}, if let periodSectionView = self.periodSection.view {
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) if periodSectionView.superview == nil {
) periodSectionView.layer.allowsGroupOpacity = true
let permissionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: permissionsSectionSize) self.scrollView.addSubview(periodSectionView)
if let permissionsSectionView = self.permissionsSection.view { }
if permissionsSectionView.superview == nil { transition.setFrame(view: periodSectionView, frame: periodSectionFrame)
permissionsSectionView.layer.allowsGroupOpacity = true alphaTransition.setAlpha(view: periodSectionView, alpha: self.isOn ? 1.0 : 0.0)
self.scrollView.addSubview(permissionsSectionView)
} }
transition.setFrame(view: permissionsSectionView, frame: permissionsSectionFrame) otherSectionsHeight += periodSectionSize.height
otherSectionsHeight += sectionSpacing
alphaTransition.setAlpha(view: permissionsSectionView, alpha: self.isOn ? 1.0 : 0.0)
} }
otherSectionsHeight += permissionsSectionSize.height*/
if self.isOn { if self.isOn {
contentHeight += otherSectionsHeight contentHeight += otherSectionsHeight
@ -799,6 +1204,7 @@ final class GreetingMessageSetupScreenComponent: Component {
let previousBounds = self.scrollView.bounds let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight) let contentSize = CGSize(width: availableSize.width, height: contentHeight)
self.ignoreScrolling = true
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
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 { if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets self.scrollView.scrollIndicatorInsets = scrollInsets
} }
self.ignoreScrolling = false
if !previousBounds.isEmpty, !transition.animation.isImmediate { if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds let bounds = self.scrollView.bounds
@ -836,13 +1243,19 @@ final class GreetingMessageSetupScreenComponent: Component {
} }
public final class GreetingMessageSetupScreen: ViewControllerComponentContainer { public final class GreetingMessageSetupScreen: ViewControllerComponentContainer {
public enum Mode {
case greeting
case away
}
private let context: AccountContext private let context: AccountContext
public init(context: AccountContext) { public init(context: AccountContext, mode: Mode) {
self.context = context self.context = context
super.init(context: context, component: GreetingMessageSetupScreenComponent( super.init(context: context, component: GreetingMessageSetupScreenComponent(
context: context context: context,
mode: mode
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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/AsyncDisplayKit",
"//submodules/Display", "//submodules/Display",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/LegacyUI", "//submodules/LegacyComponents",
"//submodules/ComponentFlow", "//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

@ -3,96 +3,69 @@ import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
import LegacyUI import LegacyComponents
import ComponentFlow import ComponentFlow
import MultilineTextComponent
final class SliderComponent: Component { public final class SliderComponent: Component {
typealias EnvironmentType = Empty 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 public init(
let value: Float valueCount: Int,
let minValue: Float value: Int,
let maxValue: Float trackBackgroundColor: UIColor,
let startValue: Float trackForegroundColor: UIColor,
let isEnabled: Bool valueUpdated: @escaping (Int) -> Void,
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 isTrackingUpdated: ((Bool) -> Void)? = nil
) { ) {
self.title = title self.valueCount = valueCount
self.value = value self.value = value
self.minValue = minValue self.trackBackgroundColor = trackBackgroundColor
self.maxValue = maxValue self.trackForegroundColor = trackForegroundColor
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.valueUpdated = valueUpdated self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated self.isTrackingUpdated = isTrackingUpdated
} }
static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool { public static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool {
if lhs.title != rhs.title { if lhs.valueCount != rhs.valueCount {
return false return false
} }
if lhs.value != rhs.value { if lhs.value != rhs.value {
return false return false
} }
if lhs.minValue != rhs.minValue { if lhs.trackBackgroundColor != rhs.trackBackgroundColor {
return false return false
} }
if lhs.maxValue != rhs.maxValue { if lhs.trackForegroundColor != rhs.trackForegroundColor {
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 {
return false return false
} }
return true return true
} }
final class View: UIView, UITextFieldDelegate { public final class View: UIView {
private let title = ComponentView<Empty>()
private let value = ComponentView<Empty>()
private var sliderView: TGPhotoEditorSliderView? private var sliderView: TGPhotoEditorSliderView?
private var component: SliderComponent? private var component: SliderComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
override init(frame: CGRect) { override public init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
} }
required init?(coder: NSCoder) { required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") 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.component = component
self.state = state self.state = state
let size = CGSize(width: availableSize.width, height: 44.0)
var internalIsTrackingUpdated: ((Bool) -> Void)? var internalIsTrackingUpdated: ((Bool) -> Void)?
if let isTrackingUpdated = component.isTrackingUpdated { if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in internalIsTrackingUpdated = { [weak self] isTracking in
@ -100,23 +73,11 @@ final class SliderComponent: Component {
if isTracking { if isTracking {
self.sliderView?.bordered = true self.sliderView?.bordered = true
} else { } else {
Queue.mainQueue().after(0.1) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
self.sliderView?.bordered = false self?.sliderView?.bordered = false
} })
} }
isTrackingUpdated(isTracking) 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 let sliderView: TGPhotoEditorSliderView
if let current = self.sliderView { if let current = self.sliderView {
sliderView = current sliderView = current
sliderView.value = CGFloat(component.value)
} else { } else {
sliderView = TGPhotoEditorSliderView() sliderView = TGPhotoEditorSliderView()
sliderView.backgroundColor = .clear
sliderView.startColor = UIColor(rgb: 0xffffff)
sliderView.enablePanHandling = true sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0 sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 2.0 sliderView.lineSize = 4.0
sliderView.minimumValue = CGFloat(component.minValue) sliderView.dotSize = 5.0
sliderView.maximumValue = CGFloat(component.maxValue) sliderView.minimumValue = 0.0
sliderView.startValue = CGFloat(component.startValue) sliderView.startValue = 0.0
sliderView.value = CGFloat(component.value) 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.disablesInteractiveTransitionGestureRecognizer = true
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
sliderView.layer.allowsGroupOpacity = true sliderView.layer.allowsGroupOpacity = true
self.sliderView = sliderView self.sliderView = sliderView
self.addSubview(sliderView) self.addSubview(sliderView)
} }
sliderView.value = CGFloat(component.value)
sliderView.interactionBegan = { sliderView.interactionBegan = {
internalIsTrackingUpdated?(true) internalIsTrackingUpdated?(true)
} }
@ -149,70 +128,17 @@ final class SliderComponent: Component {
internalIsTrackingUpdated?(false) internalIsTrackingUpdated?(false)
} }
if component.isEnabled { transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: 44.0)))
sliderView.alpha = 1.3 sliderView.hitTestEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
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: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0))) return size
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)
} }
@objc private func sliderValueChanged() { @objc private func sliderValueChanged() {
guard let component = self.component, let sliderView = self.sliderView else { guard let component = self.component, let sliderView = self.sliderView else {
return return
} }
component.valueUpdated(Float(sliderView.value)) component.valueUpdated(Int(sliderView.value))
} }
} }
@ -220,240 +146,7 @@ final class SliderComponent: Component {
return View(frame: CGRect()) return View(frame: CGRect())
} }
public func update(view: View, availableSize: CGSize, state: State, 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)
}
}
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 {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) 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 AccountContext
import UIKitRuntimeUtils import UIKitRuntimeUtils
final class TimeSelectionActionSheet: ActionSheetController { public final class TimeSelectionActionSheet: ActionSheetController {
private var presentationDisposable: Disposable? private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>() private let _ready = Promise<Bool>()
override var ready: Promise<Bool> { override public var ready: Promise<Bool> {
return self._ready 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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings 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") fatalError("init(coder:) has not been implemented")
} }

Binary file not shown.

@ -483,7 +483,7 @@ public final class AccountContextImpl: AccountContext {
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
return .thread(peerId: data.peerId, threadId: data.threadId, data: context.state) return .thread(peerId: data.peerId, threadId: data.threadId, data: context.state)
} }
case .feed: case .customChatContents:
preconditionFailure() preconditionFailure()
} }
} }
@ -509,7 +509,7 @@ public final class AccountContextImpl: AccountContext {
} else { } else {
return .single(nil) return .single(nil)
} }
case .feed: case .customChatContents:
return .single(nil) return .single(nil)
} }
} }
@ -547,7 +547,7 @@ public final class AccountContextImpl: AccountContext {
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
return context.unreadCount return context.unreadCount
} }
case .feed: case .customChatContents:
return .single(0) return .single(0)
} }
} }
@ -559,7 +559,7 @@ public final class AccountContextImpl: AccountContext {
case let .replyThread(data): case let .replyThread(data):
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
context.applyMaxReadIndex(messageIndex: messageIndex) context.applyMaxReadIndex(messageIndex: messageIndex)
case .feed: case .customChatContents:
break break
} }
} }

@ -289,7 +289,7 @@ extension ChatControllerImpl {
if let location = location { 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))) source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
} else { } 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) self.canReadHistory.set(false)

@ -137,7 +137,7 @@ public final class ChatControllerOverlayPresentationData {
enum ChatLocationInfoData { enum ChatLocationInfoData {
case peer(Promise<PeerView>) case peer(Promise<PeerView>)
case replyThread(Promise<Message?>) case replyThread(Promise<Message?>)
case feed case customChatContents
} }
enum ChatRecordingActivity { enum ChatRecordingActivity {
@ -647,10 +647,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
promise.set(.single(nil)) promise.set(.single(nil))
} }
self.chatLocationInfoData = .replyThread(promise) self.chatLocationInfoData = .replyThread(promise)
case .feed: case .customChatContents:
locationBroadcastPanelSource = .none locationBroadcastPanelSource = .none
groupCallPanelSource = .none groupCallPanelSource = .none
self.chatLocationInfoData = .feed self.chatLocationInfoData = .customChatContents
} }
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -2296,7 +2296,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
case .replyThread: case .replyThread:
postAsReply = true postAsReply = true
case .feed: case .customChatContents:
postAsReply = true postAsReply = true
} }
@ -2902,7 +2902,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
let peerId = replyThreadMessage.peerId 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) 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 break
} }
}, requestRedeliveryOfFailedMessages: { [weak self] id in }, requestRedeliveryOfFailedMessages: { [weak self] id in
@ -2943,7 +2943,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() 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.currentContextController = controller
strongSelf.forEachController({ controller in strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
@ -3021,7 +3021,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() 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.currentContextController = controller
strongSelf.forEachController({ controller in strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
@ -4787,7 +4787,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode self.avatarNode = avatarNode
case .feed: case .customChatContents:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
} }
chatInfoButtonItem.target = self chatInfoButtonItem.target = self
@ -5669,7 +5669,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
replyThreadType = .replies replyThreadType = .replies
} }
} }
case .feed: case .customChatContents:
replyThreadType = .replies 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.reportIrrelvantGeoNoticePromise.set(.single(nil))
self.titleDisposable.set(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 { if !self.didSetChatLocationInfoReady {
self.didSetChatLocationInfoReady = true self.didSetChatLocationInfoReady = true
@ -6307,7 +6319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
activitySpace = PeerActivitySpace(peerId: peerId, category: .global) activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId)) activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .feed: case .customChatContents:
activitySpace = nil activitySpace = nil
} }
@ -7763,7 +7775,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .peer: case .peer:
pinnedMessageId = topPinnedMessage?.message.id pinnedMessageId = topPinnedMessage?.message.id
pinnedMessage = topPinnedMessage pinnedMessage = topPinnedMessage
case .feed: case .customChatContents:
pinnedMessageId = nil pinnedMessageId = nil
pinnedMessage = nil pinnedMessage = nil
} }
@ -7934,7 +7946,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
$0.updatedChatHistoryState(state) $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 strongSelf.botStart = nil
if !isEmpty { if !isEmpty {
strongSelf.startBot(botStart.payload) 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 self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in
if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId { guard let strongSelf = self else {
var correlationIds: [Int64] = [] return
for message in messages { }
switch message {
case let .message(_, _, _, _, _, _, _, _, correlationId, _): var correlationIds: [Int64] = []
if let correlationId = correlationId { for message in messages {
correlationIds.append(correlationId) switch message {
} case let .message(_, _, _, _, _, _, _, _, correlationId, _):
default: if let correlationId = correlationId {
break correlationIds.append(correlationId)
} }
default:
break
} }
strongSelf.commitPurposefulAction() }
strongSelf.commitPurposefulAction()
if let peerId = strongSelf.chatLocation.peerId {
var hasDisabledContent = false var hasDisabledContent = false
if "".isEmpty { if "".isEmpty {
hasDisabledContent = false 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]) donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId])
} else if case let .customChatContents(customChatContents) = strongSelf.subject {
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) customChatContents.enqueueMessages(messages: messages)
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
} }
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
} }
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in
@ -9066,7 +9085,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return 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 |> deliverOnMainQueue).start(next: { [weak strongSelf] message in
guard let strongSelf, let message else { guard let strongSelf, let message else {
return return
@ -9161,27 +9191,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
media = .keep media = .keep
} }
let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId) if case let .customChatContents(customChatContents) = strongSelf.subject {
|> deliverOnMainQueue) customChatContents.editMessage(id: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
.startStandalone(next: { [weak self] currentMessage in
if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
if let currentMessage = currentMessage { var state = state
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? [] state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false) state = state.updatedEditMessageState(nil)
return state
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) } 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 }, beginMessageSearch: { [weak self] domain, query in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -9313,7 +9353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.updateItemNodesSearchTextHighlightStates() strongSelf.updateItemNodesSearchTextHighlightStates()
if let navigateIndex = navigateIndex { if let navigateIndex = navigateIndex {
switch strongSelf.chatLocation { switch strongSelf.chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread, .customChatContents:
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) 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) activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId)) activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .feed: case .customChatContents:
activitySpace = nil activitySpace = nil
} }
@ -12152,7 +12192,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
peerId = replyThreadMessage.peerId peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId threadId = replyThreadMessage.threadId
} }
case .feed: case .customChatContents:
return return
} }
@ -12710,7 +12750,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.effectiveNavigationController?.pushViewController(infoController) self.effectiveNavigationController?.pushViewController(infoController)
} }
} }
case .feed: case .customChatContents:
break break
} }
}) })
@ -12922,7 +12962,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})) }))
case .replyThread: case .replyThread:
break break
case .feed: case .customChatContents:
break break
} }
} }
@ -13654,7 +13694,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let effectiveMessageId = replyThreadMessage.effectiveMessageId { if let effectiveMessageId = replyThreadMessage.effectiveMessageId {
defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil) defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil)
} }
case .feed: case .customChatContents:
break break
} }
@ -13709,6 +13749,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { 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 { guard let peerId = self.chatLocation.peerId else {
return 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) 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 { } else {
source = .default source = .default
} }

@ -39,10 +39,6 @@ extension ChatControllerImpl {
} }
func presentAttachmentMenu(subject: AttachMenuSubject) { func presentAttachmentMenu(subject: AttachMenuSubject) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let context = self.context let context = self.context
let inputIsActive = self.presentationInterfaceState.inputMode == .text let inputIsActive = self.presentationInterfaceState.inputMode == .text
@ -56,42 +52,46 @@ extension ChatControllerImpl {
var bannedSendFiles: (Int32, Bool)? var bannedSendFiles: (Int32, Bool)?
var canSendPolls = true var canSendPolls = true
if let peer = peer as? TelegramUser, peer.botInfo == nil { if let peer = self.presentationInterfaceState.renderedPeer?.peer {
canSendPolls = false if let peer = peer as? TelegramUser, peer.botInfo == nil {
} 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 canSendPolls = false
} } else if peer is TelegramSecretChat {
} 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 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] var availableButtons: [AttachmentButtonType] = [.gallery, .file]
@ -111,24 +111,26 @@ extension ChatControllerImpl {
} }
var peerType: AttachMenuBots.Bot.PeerFlags = [] var peerType: AttachMenuBots.Bot.PeerFlags = []
if let user = peer as? TelegramUser { if let peer = self.presentationInterfaceState.renderedPeer?.peer {
if let _ = user.botInfo { if let user = peer as? TelegramUser {
peerType.insert(.bot) if let _ = user.botInfo {
} else { peerType.insert(.bot)
peerType.insert(.user) } else {
} peerType.insert(.user)
} else if let _ = peer as? TelegramGroup { }
peerType = .group } else if let _ = peer as? TelegramGroup {
} else if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
peerType = .channel
} else {
peerType = .group 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> 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() buttons = self.context.engine.messages.attachMenuBots()
|> map { attachMenuBots in |> map { attachMenuBots in
var buttons = availableButtons var buttons = availableButtons
@ -177,7 +179,7 @@ extension ChatControllerImpl {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let premiumGiftOptions: [CachedPremiumGiftOption] 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 premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions
} else { } else {
premiumGiftOptions = [] premiumGiftOptions = []
@ -324,20 +326,30 @@ extension ChatControllerImpl {
return return
} }
let selfPeerId: PeerId let selfPeerId: PeerId
if let peer = peer as? TelegramChannel, case .broadcast = peer.info { if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
selfPeerId = peer.id if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) { selfPeerId = peer.id
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 { } else {
selfPeerId = strongSelf.context.account.peerId selfPeerId = strongSelf.context.account.peerId
} }
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId)) 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 { guard let strongSelf = self, let selfPeer = selfPeer else {
return return
} }
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages let hasLiveLocation: Bool
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in 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 { guard let strongSelf = self else {
return return
} }
@ -523,69 +535,73 @@ extension ChatControllerImpl {
completion(controller, controller?.mediaPickerContext) completion(controller, controller?.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil) strongSelf.controllerNavigationDisposable.set(nil)
case .gift: case .gift:
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
if !premiumGiftOptions.isEmpty { let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in 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 { 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) completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil) strongSelf.controllerNavigationDisposable.set(nil)
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone() if bot.flags.contains(.notActivated) {
} let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
case let .app(bot): guard let self else {
var payload: String? return
var fromAttachMenu = true }
if case let .bot(_, botPayload, _) = subject { if bot.flags.contains(.showInSettingsDisclaimer) {
payload = botPayload let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
fromAttachMenu = false }
} let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
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) |> deliverOnMainQueue).startStandalone(error: { _ in
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject }, completed: { [weak controller] in
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller?.refresh()
controller.openUrl = { [weak self] url, concealed, commit in })
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) },
} dismissed: {
controller.getNavigationController = { [weak self] in strongSelf.attachmentController?.dismiss(animated: true)
return self?.effectiveNavigationController
}
controller.completion = { [weak self] in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
}) })
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: default:
break 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) { 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 var isScheduledMessages = false
if case .scheduledMessages = self.presentationInterfaceState.subject { if case .scheduledMessages = self.presentationInterfaceState.subject {
isScheduledMessages = true isScheduledMessages = true
@ -1073,7 +1086,7 @@ extension ChatControllerImpl {
let controller = MediaPickerScreen( let controller = MediaPickerScreen(
context: self.context, context: self.context,
updatedPresentationData: self.updatedPresentationData, updatedPresentationData: self.updatedPresentationData,
peer: EnginePeer(peer), peer: (self.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init),
threadTitle: self.threadInfo?.title, threadTitle: self.threadInfo?.title,
chatLocation: self.chatLocation, chatLocation: self.chatLocation,
isScheduledMessages: isScheduledMessages, isScheduledMessages: isScheduledMessages,

@ -59,7 +59,7 @@ extension ChatControllerImpl {
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
peerId = replyThreadMessage.peerId peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId threadId = replyThreadMessage.threadId
case .feed: case .customChatContents:
return return
} }
@ -96,7 +96,7 @@ extension ChatControllerImpl {
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
peerId = replyThreadMessage.peerId peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId threadId = replyThreadMessage.threadId
case .feed: case .customChatContents:
return return
} }

@ -33,7 +33,7 @@ extension ChatControllerImpl {
break break
case let .replyThread(replyThreadMessage): case let .replyThread(replyThreadMessage):
threadId = replyThreadMessage.threadId threadId = replyThreadMessage.threadId
case .feed: case .customChatContents:
break break
} }
@ -125,7 +125,7 @@ extension ChatControllerImpl {
}) })
if let navigateIndex = navigateIndex { if let navigateIndex = navigateIndex {
switch strongSelf.chatLocation { switch strongSelf.chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread, .customChatContents:
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
} }
} }

@ -53,7 +53,7 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
text = interfaceState.strings.ChatList_StartMessaging text = interfaceState.strings.ChatList_StartMessaging
} else { } else {
switch interfaceState.chatLocation { switch interfaceState.chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread, .customChatContents:
if case .scheduledMessages = interfaceState.subject { if case .scheduledMessages = interfaceState.subject {
text = interfaceState.strings.ScheduledMessages_EmptyPlaceholder text = interfaceState.strings.ScheduledMessages_EmptyPlaceholder
} else { } else {
@ -701,6 +701,12 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
} }
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { 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 { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings 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) 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) self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText)
let strings: [String] = [ let lines: [NSAttributedString] = strings.map {
interfaceState.strings.Conversation_ClousStorageInfo_Description1, return parseMarkdownIntoAttributedString($0, attributes: MarkdownAttributes(
interfaceState.strings.Conversation_ClousStorageInfo_Description2, body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText),
interfaceState.strings.Conversation_ClousStorageInfo_Description3, bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: serviceColor.primaryText),
interfaceState.strings.Conversation_ClousStorageInfo_Description4 link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText),
] linkAttribute: { url in
return ("URL", url)
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } }
), textAlignment: centerText ? .center : .natural)
}
for i in 0 ..< lines.count { for i in 0 ..< lines.count {
if i >= self.lineNodes.count { if i >= self.lineNodes.count {
@ -727,6 +772,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
textNode.maximumNumberOfLines = 0 textNode.maximumNumberOfLines = 0
textNode.isUserInteractionEnabled = false textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false textNode.displaysAsynchronously = false
textNode.textAlignment = centerText ? .center : .natural
self.addSubnode(textNode) self.addSubnode(textNode)
self.lineNodes.append(textNode) self.lineNodes.append(textNode)
} }
@ -751,7 +797,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
var lineNodes: [(CGSize, ImmediateTextNode)] = [] var lineNodes: [(CGSize, ImmediateTextNode)] = []
for textNode in self.lineNodes { 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) contentWidth = max(contentWidth, textSize.width)
contentHeight += textSize.height + titleSpacing contentHeight += textSize.height + titleSpacing
lineNodes.append((textSize, textNode)) lineNodes.append((textSize, textNode))
@ -1166,7 +1212,9 @@ final class ChatEmptyNode: ASDisplayNode {
case .detailsPlaceholder: case .detailsPlaceholder:
contentType = .regular contentType = .regular
case let .emptyChat(emptyType): 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 { if case .topic = emptyType {
contentType = .topic contentType = .topic
} else { } 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] { 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 return entries.map { entry -> ListViewInsertItem in
switch entry.entry { switch entry.entry {
case let .MessageEntry(message, presentationData, read, location, selection, attributes): case let .MessageEntry(message, presentationData, read, location, selection, attributes):
let item: ListViewItem let item: ListViewItem
switch mode { switch mode {
case .bubbles: 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): case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool let displayHeader: Bool
switch displayHeaders { switch displayHeaders {
@ -236,7 +241,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
let item: ListViewItem let item: ListViewItem
switch mode { switch mode {
case .bubbles: 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: case .list:
assertionFailure() assertionFailure()
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) 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] { 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 return entries.map { entry -> ListViewUpdateItem in
switch entry.entry { switch entry.entry {
case let .MessageEntry(message, presentationData, read, location, selection, attributes): case let .MessageEntry(message, presentationData, read, location, selection, attributes):
let item: ListViewItem let item: ListViewItem
switch mode { switch mode {
case .bubbles: 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): case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool let displayHeader: Bool
switch displayHeaders { switch displayHeaders {
@ -281,7 +291,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
let item: ListViewItem let item: ListViewItem
switch mode { switch mode {
case .bubbles: 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: case .list:
assertionFailure() assertionFailure()
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false) 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 { if apply {
switch strongSelf.chatLocation { switch strongSelf.chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread:
if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex) strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex)
} }
case .customChatContents:
break
} }
} }
}).strict()) }).strict())
@ -2757,7 +2769,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
switch self.chatLocation { switch self.chatLocation {
case .peer: case .peer:
messageIndex = maxIncomingIndex messageIndex = maxIncomingIndex
case .replyThread, .feed: case .replyThread, .customChatContents:
messageIndex = maxOverallIndex messageIndex = maxOverallIndex
} }
@ -3142,7 +3154,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
} }
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo) 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 { if self.currentHistoryState != historyState {
self.currentHistoryState = historyState self.currentHistoryState = historyState
self.historyState.set(historyState) self.historyState.set(historyState)
@ -3403,7 +3421,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
strongSelf.historyView = transition.historyView strongSelf.historyView = transition.historyView
let loadState: ChatHistoryNodeLoadState let loadState: ChatHistoryNodeLoadState
var alwaysHasMessages = false
if case .custom = strongSelf.source { if case .custom = strongSelf.source {
if case .customChatContents = strongSelf.chatLocation {
} else {
alwaysHasMessages = true
}
}
if alwaysHasMessages {
loadState = .messages loadState = .messages
} else if let historyView = strongSelf.historyView { } else if let historyView = strongSelf.historyView {
if historyView.filteredEntries.isEmpty { if historyView.filteredEntries.isEmpty {
@ -3513,7 +3538,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
switch strongSelf.chatLocation { switch strongSelf.chatLocation {
case .peer: case .peer:
messageIndex = incomingIndex messageIndex = incomingIndex
case .replyThread, .feed: case .replyThread, .customChatContents:
messageIndex = overallIndex messageIndex = overallIndex
} }
@ -3534,7 +3559,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
} }
strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages))) strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo) 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 { if strongSelf.currentHistoryState != historyState {
strongSelf.currentHistoryState = historyState strongSelf.currentHistoryState = historyState
strongSelf.historyState.set(historyState) strongSelf.historyState.set(historyState)
@ -4027,6 +4056,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if let messageItem = messageItem { if let messageItem = messageItem {
let associatedData = messageItem.associatedData let associatedData = messageItem.associatedData
let disableFloatingDateHeaders = messageItem.disableDate
loop: for i in 0 ..< historyView.filteredEntries.count { loop: for i in 0 ..< historyView.filteredEntries.count {
switch historyView.filteredEntries[i] { switch historyView.filteredEntries[i] {
@ -4036,7 +4066,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let item: ListViewItem let item: ListViewItem
switch self.mode { switch self.mode {
case .bubbles: 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): case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool let displayHeader: Bool
switch displayHeaders { switch displayHeaders {
@ -4083,6 +4113,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if let messageItem = messageItem { if let messageItem = messageItem {
let associatedData = messageItem.associatedData let associatedData = messageItem.associatedData
let disableFloatingDateHeaders = messageItem.disableDate
loop: for i in 0 ..< historyView.filteredEntries.count { loop: for i in 0 ..< historyView.filteredEntries.count {
switch historyView.filteredEntries[i] { switch historyView.filteredEntries[i] {
@ -4092,7 +4123,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let item: ListViewItem let item: ListViewItem
switch self.mode { switch self.mode {
case .bubbles: 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): case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool let displayHeader: Bool
switch displayHeaders { switch displayHeaders {

@ -333,7 +333,7 @@ private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatL
readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalState: totalUnreadState, notificationSettings: notificationSettings) readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalState: totalUnreadState, notificationSettings: notificationSettings)
} }
} }
case .replyThread, .feed: case .replyThread, .customChatContents:
break break
} }
default: default:

@ -337,7 +337,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
} }
case .replyThread: case .replyThread:
canReply = true canReply = true
case .feed: case .customChatContents:
canReply = false canReply = false
} }
return canReply return canReply
@ -597,6 +597,68 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return .single(ContextController.Items(content: .list(actions))) 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 loadStickerSaveStatus: MediaId?
var loadCopyMediaResource: MediaResource? var loadCopyMediaResource: MediaResource?
var isAction = false var isAction = false
@ -654,7 +716,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
canPin = false canPin = false
} else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty { } else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty {
switch chatPresentationInterfaceState.chatLocation { switch chatPresentationInterfaceState.chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread, .customChatContents:
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel { if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
if !isAction { if !isAction {
canPin = channel.hasPermission(.pinMessages) canPin = channel.hasPermission(.pinMessages)

@ -368,7 +368,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartPanel = true 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 { if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartPanel = true 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 { if case .inline = chatPresentationInterfaceState.mode {
displayInputTextPanel = false 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 return nil
} }
@ -95,7 +104,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
var hasMessages = false var hasMessages = false
if let chatHistoryState = presentationInterfaceState.chatHistoryState { if let chatHistoryState = presentationInterfaceState.chatHistoryState {
if case .loaded(false) = chatHistoryState { if case .loaded(false, _) = chatHistoryState {
hasMessages = true hasMessages = true
} }
} }
@ -108,6 +117,16 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
return nil 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 case .replyThread = presentationInterfaceState.chatLocation {
if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
} else if hasMessages { } else if hasMessages {

@ -34,6 +34,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
let blurBackground: Bool = true let blurBackground: Bool = true
let centerVertically: Bool let centerVertically: Bool
private weak var chatController: ChatControllerImpl?
private weak var chatNode: ChatControllerNode? private weak var chatNode: ChatControllerNode?
private let engine: TelegramEngine private let engine: TelegramEngine
private let message: Message private let message: Message
@ -43,6 +44,9 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
if self.message.adAttribute != nil { if self.message.adAttribute != nil {
return .single(false) 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)) return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: self.message.id))
|> map { message -> Bool in |> map { message -> Bool in
@ -55,7 +59,8 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|> distinctUntilChanged |> 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.chatNode = chatNode
self.engine = engine self.engine = engine
self.message = message 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) 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 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) self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern)
let placeholderText: String let placeholderText: String
switch chatLocation { switch chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread, .customChatContents:
if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags { if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags {
if case .standard(.embedded(false)) = presentationInterfaceState.mode { if case .standard(.embedded(false)) = presentationInterfaceState.mode {
placeholderText = strings.Common_Search placeholderText = strings.Common_Search
@ -114,7 +114,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
self.searchBar.prefixString = nil self.searchBar.prefixString = nil
let placeholderText: String let placeholderText: String
switch self.chatLocation { switch self.chatLocation {
case .peer, .replyThread, .feed: case .peer, .replyThread, .customChatContents:
if presentationInterfaceState.historyFilter != nil { if presentationInterfaceState.historyFilter != nil {
placeholderText = self.strings.Common_Search placeholderText = self.strings.Common_Search
} else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags { } else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags {

@ -1517,7 +1517,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
} else { } else {
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { 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 displayBotStartButton = true
} else if interfaceState.peerIsBlocked { } else if interfaceState.peerIsBlocked {
displayBotStartButton = true displayBotStartButton = true
@ -1801,38 +1801,56 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId
let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject 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 self.initializedPlaceholder = true
var placeholder: String var placeholder: String = ""
if let channel = peer as? TelegramChannel, case .broadcast = channel.info { if let peer = interfaceState.renderedPeer?.peer {
if interfaceState.interfaceState.silentPosting { if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder if interfaceState.interfaceState.silentPosting {
} else { placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
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 { } 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) return BusinessHoursSetupScreen(context: context)
} }
public func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController { public func makeGreetingMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController {
return GreetingMessageSetupScreen(context: context) 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 { public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController {