[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

View File

@ -395,13 +395,13 @@ public enum ChatSearchDomain: Equatable {
public enum ChatLocation: Equatable {
case peer(id: PeerId)
case replyThread(message: ChatReplyThreadMessage)
case feed(id: Int32)
case customChatContents
}
public extension ChatLocation {
var normalized: ChatLocation {
switch self {
case .peer, .feed:
case .peer, .customChatContents:
return self
case let .replyThread(message):
return .replyThread(message: message.normalized)
@ -936,7 +936,8 @@ public protocol SharedAccountContext: AnyObject {
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController
func makeBusinessHoursSetupScreen(context: AccountContext) -> ViewController
func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController
func makeGreetingMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController
func makeQuickReplySetupScreen(context: AccountContext) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>

View File

@ -744,6 +744,7 @@ public enum ChatControllerSubject: Equatable {
case scheduledMessages
case pinnedMessages(id: EngineMessage.Id?)
case messageOptions(peerIds: [EnginePeer.Id], ids: [EngineMessage.Id], info: MessageOptionsInfo)
case customChatContents(contents: ChatCustomContentsProtocol)
public static func ==(lhs: ChatControllerSubject, rhs: ChatControllerSubject) -> Bool {
switch lhs {
@ -771,6 +772,12 @@ public enum ChatControllerSubject: Equatable {
} else {
return false
}
case let .customChatContents(lhsValue):
if case let .customChatContents(rhsValue) = rhs, lhsValue === rhsValue {
return true
} else {
return false
}
}
}
@ -1050,7 +1057,23 @@ public enum ChatHistoryListSource {
}
case `default`
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, quote: Quote?, loadMore: (() -> Void)?)
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, loadMore: (() -> Void)?)
}
public enum ChatCustomContentsKind: Equatable {
case greetingMessageInput
case awayMessageInput
case quickReplyMessageInput(shortcut: String)
}
public protocol ChatCustomContentsProtocol: AnyObject {
var kind: ChatCustomContentsKind { get }
var messages: Signal<[Message], NoError> { get }
var messageLimit: Int? { get }
func enqueueMessages(messages: [EnqueueMessage])
func deleteMessages(ids: [EngineMessage.Id])
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool)
}
public enum ChatHistoryListDisplayHeaders {
@ -1069,7 +1092,7 @@ public protocol ChatControllerInteractionProtocol: AnyObject {
public enum ChatHistoryNodeHistoryState: Equatable {
case loading
case loaded(isEmpty: Bool)
case loaded(isEmpty: Bool, hasReachedLimits: Bool)
}
public protocol ChatHistoryListNode: ListView {

View File

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

View File

@ -26,6 +26,7 @@ import TextNodeWithEntities
import ComponentFlow
import EmojiStatusComponent
import AvatarVideoNode
import AppBundle
public enum ChatListItemContent {
public struct ThreadInfo: Equatable {
@ -89,6 +90,16 @@ public enum ChatListItemContent {
}
}
public struct CustomMessageListData: Equatable {
public var commandPrefix: String?
public var messageCount: Int?
public init(commandPrefix: String?, messageCount: Int?) {
self.commandPrefix = commandPrefix
self.messageCount = messageCount
}
}
public struct PeerData {
public var messages: [EngineMessage]
public var peer: EngineRenderedPeer
@ -112,6 +123,7 @@ public enum ChatListItemContent {
public var requiresPremiumForMessaging: Bool
public var displayAsTopicList: Bool
public var tags: [Tag]
public var customMessageListData: CustomMessageListData?
public init(
messages: [EngineMessage],
@ -135,7 +147,8 @@ public enum ChatListItemContent {
storyState: StoryState?,
requiresPremiumForMessaging: Bool,
displayAsTopicList: Bool,
tags: [Tag]
tags: [Tag],
customMessageListData: CustomMessageListData? = nil
) {
self.messages = messages
self.peer = peer
@ -159,6 +172,7 @@ public enum ChatListItemContent {
self.requiresPremiumForMessaging = requiresPremiumForMessaging
self.displayAsTopicList = displayAsTopicList
self.tags = tags
self.customMessageListData = customMessageListData
}
}
@ -1153,6 +1167,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode
var dateStatusIconNode: ASImageNode?
var dateDisclosureIconView: UIImageView?
let separatorNode: ASDisplayNode
let statusNode: ChatListStatusNode
let badgeNode: ChatListBadgeNode
@ -1587,7 +1602,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if peer.id.isReplies {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
} else if peer.id.isReplies {
overrideImage = .repliesIcon
} else if peer.id.isAnonymousSavedMessages {
overrideImage = .anonymousSavedMessagesIcon
@ -2021,15 +2037,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let enableChatListPhotos = true
let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0))
let avatarLeftInset: CGFloat
if item.interaction.isInlineMode {
avatarLeftInset = 12.0
} else if !useChatListLayout {
avatarLeftInset = 50.0
} else {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
avatarDiameter = 40.0
avatarLeftInset = 18.0 + avatarDiameter
} else {
if item.interaction.isInlineMode {
avatarLeftInset = 12.0
} else if !useChatListLayout {
avatarLeftInset = 50.0
} else {
avatarLeftInset = 18.0 + avatarDiameter
}
}
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
@ -2083,7 +2105,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
hideAuthor = true
}
let attributedText: NSAttributedString
var attributedText: NSAttributedString
var hasDraft = false
var inlineAuthorPrefix: String?
@ -2401,6 +2423,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
attributedText = composedString
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, let commandPrefix = customMessageListData.commandPrefix {
let mutableAttributedText = NSMutableAttributedString(attributedString: attributedText)
let boldTextFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
mutableAttributedText.insert(NSAttributedString(string: commandPrefix + " ", font: boldTextFont, textColor: theme.titleColor), at: 0)
attributedText = mutableAttributedText
}
if !ignoreForwardedIcon {
if case .savedMessagesChats = item.chatListLocation {
displayForwardedIcon = false
@ -2548,7 +2577,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch contentData {
case let .chat(itemPeer, threadInfo, _, _, _, _, _):
if let threadInfo = threadInfo {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.commandPrefix != nil {
titleAttributedString = nil
} else {
if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) {
let textColor: UIColor
if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat {
textColor = theme.secretTitleColor
} else {
textColor = theme.titleColor
}
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: textColor)
}
}
} else if let threadInfo = threadInfo {
titleAttributedString = NSAttributedString(string: threadInfo.info.title, font: titleFont, textColor: theme.titleColor)
} else if let message = messages.last, case let .user(author) = message.author, displayAsMessage {
titleAttributedString = NSAttributedString(string: author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer.user(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: titleFont, textColor: theme.titleColor)
@ -2587,7 +2630,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
case let .peer(peerData):
topIndex = peerData.messages.first?.index
}
if let topIndex {
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if let messageCount = customMessageListData.messageCount {
dateText = "\(messageCount)"
} else {
dateText = " "
}
} else if let topIndex {
var t = Int(topIndex.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
@ -2754,7 +2803,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content {
case let .peer(peerData):
if let peer = peerData.messages.last?.author {
if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
currentCredibilityIconContent = nil
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
currentCredibilityIconContent = nil
} else if peer.isScam {
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
@ -2776,7 +2827,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
break
}
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
currentCredibilityIconContent = nil
} else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId {
currentCredibilityIconContent = nil
} else if peer.isScam {
currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
@ -3066,16 +3119,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
animateContent = true
}
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (measureLayout, measureApply) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: " ", font: titleFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = -1.0
let authorSpacing: CGFloat = -3.0
var itemHeight: CGFloat = 8.0 * 2.0 + 1.0
itemHeight -= 21.0
itemHeight += titleLayout.size.height
itemHeight += measureLayout.size.height * 3.0
itemHeight += titleSpacing
itemHeight += authorSpacing
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil {
itemHeight += measureLayout.size.height * 2.0
itemHeight += 22.0
} else {
itemHeight += titleLayout.size.height
itemHeight += measureLayout.size.height * 3.0
itemHeight += titleSpacing
itemHeight += authorSpacing
}
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0))
@ -3466,7 +3524,32 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let _ = mentionBadgeApply(animateBadges, true)
let _ = onlineApply(animateContent && animateOnline)
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size))
var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil {
dateFrame.origin.x -= 10.0
let dateDisclosureIconView: UIImageView
if let current = strongSelf.dateDisclosureIconView {
dateDisclosureIconView = current
} else {
dateDisclosureIconView = UIImageView(image: UIImage(bundleImageName: "Item List/DisclosureArrow")?.withRenderingMode(.alwaysTemplate))
strongSelf.dateDisclosureIconView = dateDisclosureIconView
strongSelf.mainContentContainerNode.view.addSubview(dateDisclosureIconView)
}
dateDisclosureIconView.tintColor = item.presentationData.theme.list.disclosureArrowColor
let iconScale: CGFloat = 0.7
if let image = dateDisclosureIconView.image {
let imageSize = CGSize(width: floor(image.size.width * iconScale), height: floor(image.size.height * iconScale))
let iconFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - imageSize.width + 4.0, y: floorToScreenPixels(dateFrame.midY - imageSize.height * 0.5)), size: imageSize)
dateDisclosureIconView.frame = iconFrame
}
} else if let dateDisclosureIconView = strongSelf.dateDisclosureIconView {
strongSelf.dateDisclosureIconView = nil
dateDisclosureIconView.removeFromSuperview()
}
transition.updateFrame(node: strongSelf.dateNode, frame: dateFrame)
var statusOffset: CGFloat = 0.0
if let dateIconImage {
@ -3997,6 +4080,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0)
}
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.messageCount != nil {
strongSelf.separatorNode.isHidden = true
}
}
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: itemHeight)))
let backgroundColor: UIColor
let highlightedBackgroundColor: UIColor
@ -4012,7 +4101,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor
}
} else {
backgroundColor = theme.itemBackgroundColor
if case let .peer(peerData) = item.content, peerData.customMessageListData != nil {
backgroundColor = .clear
} else {
backgroundColor = theme.itemBackgroundColor
}
highlightedBackgroundColor = theme.itemHighlightedBackgroundColor
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2997,6 +2997,8 @@ final class PostboxImpl {
private func internalTransaction<T>(_ f: (Transaction) -> T) -> (result: T, updatedTransactionStateVersion: Int64?, updatedMasterClientId: Int64?) {
let _ = self.isInTransaction.swap(true)
let startTime = CFAbsoluteTimeGetCurrent()
self.valueBox.begin()
let transaction = Transaction(queue: self.queue, postbox: self)
self.afterBegin(transaction: transaction)
@ -3005,6 +3007,12 @@ final class PostboxImpl {
transaction.disposed = true
self.valueBox.commit()
let endTime = CFAbsoluteTimeGetCurrent()
let transactionDuration = endTime - startTime
if transactionDuration > 0.1 {
postboxLog("Postbox transaction took \(transactionDuration * 1000.0) ms")
}
let _ = self.isInTransaction.swap(false)
if let currentUpdatedState = self.currentUpdatedState {
@ -3079,7 +3087,7 @@ final class PostboxImpl {
switch chatLocation {
case let .peer(peerId, threadId):
return .single((.peer(peerId: peerId, threadId: threadId), false))
case .thread(_, _, let data), .feed(_, let data):
case .thread(_, _, let data):
return Signal { subscriber in
var isHoleFill = false
return (data
@ -3089,6 +3097,9 @@ final class PostboxImpl {
return (.external(value), wasHoleFill)
}).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
}
case .customChatContents:
assert(false)
return .never()
}
}

View File

@ -81,7 +81,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch
return ActionDisposable {
disposable.dispose()
}
case .feed:
case .customChatContents:
subscriber.putNext(([], true))
return ActionDisposable {

View File

@ -179,7 +179,7 @@ private func wrappedHistoryViewAdditionalData(chatLocation: ChatLocationInput, a
result.append(.peerChatState(peerId))
}
}
case .feed:
case .customChatContents:
break
}
return result
@ -1839,7 +1839,7 @@ public final class AccountViewTracker {
if peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: chatLocation)
}
case .feed:
case .customChatContents:
break
}
}
@ -1852,7 +1852,7 @@ public final class AccountViewTracker {
peerId = peerIdValue
case let .thread(peerIdValue, _, _):
peerId = peerIdValue
case .feed:
case .customChatContents:
peerId = nil
}
if let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel {

View File

@ -107,6 +107,46 @@ public func stringForMonth(strings: PresentationStrings, month: Int32, ofYear ye
}
}
private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String {
switch index {
case 0:
return strings.Month_ShortJanuary
case 1:
return strings.Month_ShortFebruary
case 2:
return strings.Month_ShortMarch
case 3:
return strings.Month_ShortApril
case 4:
return strings.Month_ShortMay
case 5:
return strings.Month_ShortJune
case 6:
return strings.Month_ShortJuly
case 7:
return strings.Month_ShortAugust
case 8:
return strings.Month_ShortSeptember
case 9:
return strings.Month_ShortOctober
case 10:
return strings.Month_ShortNovember
case 11:
return strings.Month_ShortDecember
default:
return ""
}
}
public func stringForCompactDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String {
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
//TODO:localize
return "\(shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)) \(timeinfo.tm_mday) \(monthAtIndex(Int(timeinfo.tm_mon), strings: strings))"
}
public enum RelativeTimestampFormatDay {
case today
case yesterday
@ -362,37 +402,6 @@ public func stringForRelativeLiveLocationUpdateTimestamp(strings: PresentationSt
}
}
private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String {
switch index {
case 0:
return strings.Month_GenJanuary
case 1:
return strings.Month_GenFebruary
case 2:
return strings.Month_GenMarch
case 3:
return strings.Month_GenApril
case 4:
return strings.Month_GenMay
case 5:
return strings.Month_GenJune
case 6:
return strings.Month_GenJuly
case 7:
return strings.Month_GenAugust
case 8:
return strings.Month_GenSeptember
case 9:
return strings.Month_GenOctober
case 10:
return strings.Month_GenNovember
case 11:
return strings.Month_GenDecember
default:
return ""
}
}
public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
let difference = timestamp - relativeTimestamp
if difference < 60 {

View File

@ -818,8 +818,6 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if !isBroadcastChannel {
hasAvatar = true
} else if case .feed = item.chatLocation {
hasAvatar = true
}
}
} else if incoming {
@ -844,8 +842,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else if incoming {
hasAvatar = true
}
case .feed:
hasAvatar = true
case .customChatContents:
hasAvatar = false
}
if hasAvatar {
@ -1445,6 +1443,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let dateAndStatusFrame = CGRect(origin: CGPoint(x: max(displayLeftInset, updatedImageFrame.maxX - dateAndStatusSize.width - 4.0), y: updatedImageFrame.maxY - dateAndStatusSize.height - 4.0 + imageBottomPadding), size: dateAndStatusSize)
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
dateAndStatusApply(animation)
if case .customChatContents = item.associatedData.subject {
strongSelf.dateAndStatusNode.isHidden = true
}
if needsReplyBackground {
if strongSelf.replyBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {

View File

@ -675,7 +675,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
}
var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))?
if case let .linear(_, bottom) = position {
if case .customChatContents = associatedData.subject {
} else if case let .linear(_, bottom) = position {
switch bottom {
case .None, .Neighbour(_, .footer, _):
if message.adAttribute == nil {

View File

@ -1446,8 +1446,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if !isBroadcastChannel {
hasAvatar = incoming
} else if case .feed = item.chatLocation {
hasAvatar = true
} else if case .customChatContents = item.chatLocation {
hasAvatar = false
}
}
} else if incoming {
@ -2072,7 +2072,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
maximumNodeWidth = size.width
if mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment {
if case .customChatContents = item.associatedData.subject {
} else if mosaicRange.upperBound == contentPropertiesAndLayouts.count || contentPropertiesAndLayouts[contentPropertiesAndLayouts.count - 1].3.isAttachment {
let message = item.content.firstMessage
var edited = false

View File

@ -252,7 +252,10 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch position {
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
@ -267,6 +270,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
}
default:
statusType = nil
}
}
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?

View File

@ -112,8 +112,11 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
incoming = false
}
let statusType: ChatMessageDateAndStatusType?
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
@ -127,6 +130,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
}
default:
statusType = nil
}
}
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)

View File

@ -201,21 +201,25 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN
}
let statusType: ChatMessageDateAndStatusType?
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)

View File

@ -321,8 +321,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
if !isBroadcastChannel {
hasAvatar = true
} else if case .feed = item.chatLocation {
hasAvatar = true
} else if case .customChatContents = item.chatLocation {
hasAvatar = false
}
}
} else if incoming {

View File

@ -949,6 +949,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
}
if case .customChatContents = item.associatedData.subject {
strongSelf.dateAndStatusNode.isHidden = true
}
if let videoNode = strongSelf.videoNode {
videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
if strongSelf.imageScale != imageScale {

View File

@ -365,7 +365,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
}
self.avatarHeader = avatarHeader
var headers: [ListViewItemHeader] = [self.dateHeader]
var headers: [ListViewItemHeader] = []
if !self.disableDate {
headers.append(self.dateHeader)
}
if case .messageOptions = associatedData.subject {
headers = []
}

View File

@ -223,7 +223,10 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch position {
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil {
if incoming {
@ -252,6 +255,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
}
default:
statusType = nil
}
}
var statusSize = CGSize()

View File

@ -284,7 +284,10 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch preparePosition {
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if item.message.effectivelyIncoming(item.context.account.peerId) {
statusType = .ImageIncoming
@ -301,6 +304,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
statusType = nil
default:
statusType = nil
}
}
var isReplyThread = false

View File

@ -986,7 +986,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch position {
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
@ -1001,8 +1004,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
default:
statusType = nil
}
}
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
if let statusType = statusType {

View File

@ -77,21 +77,25 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.isSending && !message.isSentOrAcknowledged {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.isSending && !message.isSentOrAcknowledged {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
let entities = [MessageTextEntity(range: 0..<rawText.count, type: .Italic)]

View File

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

View File

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

View File

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

View File

@ -3,379 +3,141 @@ import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import LegacyUI
import ComponentFlow
import MultilineTextComponent
import ListSectionComponent
import SliderComponent
final class ListItemSliderSelectorComponent: Component {
typealias EnvironmentType = Empty
public final class ListItemSliderSelectorComponent: Component {
public let theme: PresentationTheme
public let values: [String]
public let selectedIndex: Int
public let selectedIndexUpdated: (Int) -> Void
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
let isEnabled: Bool
let trackColor: UIColor?
let displayValue: Bool
let valueUpdated: (Float) -> Void
let isTrackingUpdated: ((Bool) -> Void)?
init(
title: String,
value: Float,
minValue: Float,
maxValue: Float,
startValue: Float,
isEnabled: Bool,
trackColor: UIColor?,
displayValue: Bool,
valueUpdated: @escaping (Float) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil
public init(
theme: PresentationTheme,
values: [String],
selectedIndex: Int,
selectedIndexUpdated: @escaping (Int) -> Void
) {
self.title = title
self.value = value
self.minValue = minValue
self.maxValue = maxValue
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
self.theme = theme
self.values = values
self.selectedIndex = selectedIndex
self.selectedIndexUpdated = selectedIndexUpdated
}
static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool {
if lhs.title != rhs.title {
public static func ==(lhs: ListItemSliderSelectorComponent, rhs: ListItemSliderSelectorComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.value != rhs.value {
if lhs.values != rhs.values {
return false
}
if lhs.minValue != rhs.minValue {
return false
}
if lhs.maxValue != rhs.maxValue {
return false
}
if lhs.startValue != rhs.startValue {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.trackColor != rhs.trackColor {
return false
}
if lhs.displayValue != rhs.displayValue {
if lhs.selectedIndex != rhs.selectedIndex {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let title = ComponentView<Empty>()
private let value = ComponentView<Empty>()
private var sliderView: TGPhotoEditorSliderView?
public final class View: UIView, ListSectionComponent.ChildView {
private var titles: [ComponentView<Empty>] = []
private var slider = ComponentView<Empty>()
private var component: ListItemSliderSelectorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
override public init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
func update(component: ListItemSliderSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
var internalIsTrackingUpdated: ((Bool) -> Void)?
if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in
if let self {
if isTracking {
self.sliderView?.bordered = true
} else {
Queue.mainQueue().after(0.1) {
self.sliderView?.bordered = false
}
}
isTrackingUpdated(isTracking)
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
}
if let valueView = self.value.view {
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
let sliderView: TGPhotoEditorSliderView
if let current = self.sliderView {
sliderView = current
sliderView.value = CGFloat(component.value)
} else {
sliderView = TGPhotoEditorSliderView()
sliderView.backgroundColor = .clear
sliderView.startColor = UIColor(rgb: 0xffffff)
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = CGFloat(component.minValue)
sliderView.maximumValue = CGFloat(component.maxValue)
sliderView.startValue = CGFloat(component.startValue)
sliderView.value = CGFloat(component.value)
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
sliderView.layer.allowsGroupOpacity = true
self.sliderView = sliderView
self.addSubview(sliderView)
}
sliderView.interactionBegan = {
internalIsTrackingUpdated?(true)
}
sliderView.interactionEnded = {
internalIsTrackingUpdated?(false)
}
let sideInset: CGFloat = 13.0
let titleSideInset: CGFloat = 20.0
let titleClippingSideInset: CGFloat = 14.0
if component.isEnabled {
sliderView.alpha = 1.3
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
sliderView.isUserInteractionEnabled = true
} else {
sliderView.trackColor = UIColor(rgb: 0xffffff)
sliderView.alpha = 0.3
sliderView.isUserInteractionEnabled = false
}
let titleAreaWidth: CGFloat = availableSize.width - titleSideInset * 2.0
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
}
let valueText: String
if component.displayValue {
if component.value > 0.005 {
valueText = String(format: "+%.2f", component.value)
} else if component.value < -0.005 {
valueText = String(format: "%.2f", component.value)
for i in 0 ..< component.values.count {
var titleTransition = transition
let title: ComponentView<Empty>
if self.titles.count > i {
title = self.titles[i]
} else {
valueText = ""
titleTransition = titleTransition.withAnimation(.none)
title = ComponentView()
self.titles.append(title)
}
} else {
valueText = ""
}
let valueSize = self.value.update(
transition: .immediate,
component: AnyComponent(
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xf8d74a))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let valueView = self.value.view {
if valueView.superview == nil {
self.addSubview(valueView)
}
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
}
return CGSize(width: availableSize.width, height: 52.0)
}
@objc private func sliderValueChanged() {
guard let component = self.component, let sliderView = self.sliderView else {
return
}
component.valueUpdated(Float(sliderView.value))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
struct AdjustmentTool: Equatable {
let key: EditorToolKey
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
}
final class AdjustmentsComponent: Component {
typealias EnvironmentType = Empty
let tools: [AdjustmentTool]
let valueUpdated: (EditorToolKey, Float) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
tools: [AdjustmentTool],
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.tools = tools
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
if lhs.tools != rhs.tools {
return false
}
return true
}
final class View: UIView {
private let scrollView = UIScrollView()
private var toolViews: [ComponentView<Empty>] = []
private var component: AdjustmentsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let valueUpdated = component.valueUpdated
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
component.isTrackingUpdated(isTracking)
if let self {
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
if tool.key != trackingTool && i < self.toolViews.count {
if let view = self.toolViews[i].view {
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
}
var sizes: [CGSize] = []
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
let componentView: ComponentView<Empty>
if i >= self.toolViews.count {
componentView = ComponentView<Empty>()
self.toolViews.append(componentView)
} else {
componentView = self.toolViews[i]
}
var valueIsNegative = false
var value = tool.value
if case .enhance = tool.key {
if value < 0.0 {
valueIsNegative = true
}
value = abs(value)
}
let size = componentView.update(
transition: transition,
component: AnyComponent(
ListItemSliderSelectorComponent(
title: tool.title,
value: value,
minValue: tool.minValue,
maxValue: tool.maxValue,
startValue: tool.startValue,
isEnabled: true,
trackColor: nil,
displayValue: true,
valueUpdated: { value in
var updatedValue = value
if valueIsNegative {
updatedValue *= -1.0
}
valueUpdated(tool.key, updatedValue)
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(tool.key, isTracking)
}
)
),
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.values[i], font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: availableSize
containerSize: CGSize(width: 100.0, height: 100.0)
)
sizes.append(size)
}
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
for i in 0 ..< component.tools.count {
let size = sizes[i]
let componentView = self.toolViews[i]
if let view = componentView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
var titleFrame = CGRect(origin: CGPoint(x: titleSideInset - floor(titleSize.width * 0.5), y: 14.0), size: titleSize)
if component.values.count > 1 {
titleFrame.origin.x += floor(CGFloat(i) / CGFloat(component.values.count - 1) * titleAreaWidth)
}
origin = origin.offsetBy(dx: 0.0, dy: size.height)
if titleFrame.minX < titleClippingSideInset {
titleFrame.origin.x = titleSideInset
}
if titleFrame.maxX > availableSize.width - titleClippingSideInset {
titleFrame.origin.x = availableSize.width - titleClippingSideInset - titleSize.width
}
if let titleView = title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
titleTransition.setPosition(view: titleView, position: titleFrame.center)
}
}
if self.titles.count > component.values.count {
for i in component.values.count ..< self.titles.count {
self.titles[i].view?.removeFromSuperview()
}
self.titles.removeLast(self.titles.count - component.values.count)
}
let size = CGSize(width: availableSize.width, height: 180.0)
let contentSize = CGSize(width: availableSize.width, height: origin.y)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
let sliderSize = self.slider.update(
transition: transition,
component: AnyComponent(SliderComponent(
valueCount: component.values.count,
value: component.selectedIndex,
trackBackgroundColor: component.theme.list.controlSecondaryColor,
trackForegroundColor: component.theme.list.itemAccentColor,
valueUpdated: { [weak self] value in
guard let self, let component = self.component else {
return
}
component.selectedIndexUpdated(value)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
let sliderFrame = CGRect(origin: CGPoint(x: sideInset, y: 36.0), size: sliderSize)
if let sliderView = self.slider.view {
if sliderView.superview == nil {
self.addSubview(sliderView)
}
transition.setFrame(view: sliderView, frame: sliderFrame)
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
return size
self.separatorInset = 16.0
return CGSize(width: availableSize.width, height: 88.0)
}
}
@ -383,78 +145,7 @@ final class AdjustmentsComponent: Component {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class AdjustmentsScreenComponent: Component {
typealias EnvironmentType = Empty
let toggleUneditedPreview: (Bool) -> Void
init(
toggleUneditedPreview: @escaping (Bool) -> Void
) {
self.toggleUneditedPreview = toggleUneditedPreview
}
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var component: AdjustmentsScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.05
self.addGestureRecognizer(longPressGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.toggleUneditedPreview(true)
case .ended, .cancelled:
component.toggleUneditedPreview(false)
default:
break
}
}
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import LottieComponent
import Markdown
import LocationUI
import TelegramStringFormatting
import TimezoneSelectionScreen
final class BusinessHoursSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -75,7 +76,9 @@ final class BusinessHoursSetupScreenComponent: Component {
private let subtitle = ComponentView<Empty>()
private let generalSection = ComponentView<Empty>()
private let daysSection = ComponentView<Empty>()
private let timezoneSection = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var isUpdating: Bool = false
private var component: BusinessHoursSetupScreenComponent?
@ -84,6 +87,7 @@ final class BusinessHoursSetupScreenComponent: Component {
private var showHours: Bool = false
private var days: [Day] = []
private var timezoneId: String
override init(frame: CGRect) {
self.scrollView = ScrollView()
@ -98,6 +102,8 @@ final class BusinessHoursSetupScreenComponent: Component {
}
self.scrollView.alwaysBounceVertical = true
self.timezoneId = TimeZone.current.identifier
super.init(frame: frame)
self.scrollView.delegate = self
@ -126,7 +132,9 @@ final class BusinessHoursSetupScreenComponent: Component {
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
var scrolledUp = true
@ -320,6 +328,8 @@ final class BusinessHoursSetupScreenComponent: Component {
contentHeight += generalSectionSize.height
contentHeight += sectionSpacing
var daysContentHeight: CGFloat = 0.0
var daysSectionItems: [AnyComponentWithIdentity<Empty>] = []
for day in self.days {
let dayIndex = daysSectionItems.count
@ -441,7 +451,7 @@ final class BusinessHoursSetupScreenComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: daysSectionSize)
let daysSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: daysSectionSize)
if let daysSectionView = self.daysSection.view {
if daysSectionView.superview == nil {
daysSectionView.layer.allowsGroupOpacity = true
@ -452,13 +462,80 @@ final class BusinessHoursSetupScreenComponent: Component {
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
alphaTransition.setAlpha(view: daysSectionView, alpha: self.showHours ? 1.0 : 0.0)
}
daysContentHeight += daysSectionSize.height
daysContentHeight += sectionSpacing
let timezoneSectionSize = self.timezoneSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Time Zone", //TODO:localize
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: TimeZone(identifier: self.timezoneId)?.localizedName(for: .shortStandard, locale: Locale.current) ?? self.timezoneId,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
)))),
accessory: .arrow,
action: { [weak self] _ in
guard let self, let component = self.component else {
return
}
var completed: ((String) -> Void)?
let controller = TimezoneSelectionScreen(context: component.context, completed: { timezoneId in
completed?(timezoneId)
})
controller.navigationPresentation = .modal
self.environment?.controller()?.push(controller)
completed = { [weak self, weak controller] timezoneId in
guard let self else {
controller?.dismiss()
return
}
self.timezoneId = timezoneId
self.state?.updated(transition: .immediate)
controller?.dismiss()
}
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let timezoneSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + daysContentHeight), size: timezoneSectionSize)
if let timezoneSectionView = self.timezoneSection.view {
if timezoneSectionView.superview == nil {
self.scrollView.addSubview(timezoneSectionView)
}
transition.setFrame(view: timezoneSectionView, frame: timezoneSectionFrame)
let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25)
alphaTransition.setAlpha(view: timezoneSectionView, alpha: self.showHours ? 1.0 : 0.0)
}
daysContentHeight += timezoneSectionSize.height
if self.showHours {
contentHeight += daysSectionSize.height
contentHeight += daysContentHeight
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
self.ignoreScrolling = true
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
@ -472,6 +549,7 @@ final class BusinessHoursSetupScreenComponent: Component {
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
self.ignoreScrolling = false
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds

View File

@ -263,7 +263,11 @@ final class BusinessSetupScreenComponent: Component {
icon: "Settings/Menu/Photos",
title: "Quick Replies",
subtitle: "Set up shortcuts with rich text and media to respond to messages faster.",
action: {
action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
return
}
environment.controller()?.push(component.context.sharedContext.makeQuickReplySetupScreen(context: component.context))
}
))
items.append(Item(
@ -274,14 +278,18 @@ final class BusinessSetupScreenComponent: Component {
guard let self, let component = self.component, let environment = self.environment else {
return
}
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context))
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context, isAwayMode: false))
}
))
items.append(Item(
icon: "Settings/Menu/Trending",
title: "Away Messages",
subtitle: "Define messages that are automatically sent when you are off.",
action: {
action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
return
}
environment.controller()?.push(component.context.sharedContext.makeGreetingMessageSetupScreen(context: component.context, isAwayMode: true))
}
))
items.append(Item(

View File

@ -34,7 +34,17 @@ swift_library(
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
"//submodules/ShimmerEffect",
"//submodules/ChatListUI",
"//submodules/MergeLists",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/ItemListPeerActionItem",
"//submodules/ItemListUI",
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
"//submodules/DateSelectionUI",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
],
visibility = [
"//visibility:public",

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -23,6 +23,11 @@ import LottieComponent
import Markdown
import PeerListItemComponent
import AvatarNode
import ListItemSliderSelectorComponent
import DateSelectionUI
import PlainButtonComponent
import TelegramStringFormatting
import TimeSelectionActionSheet
private let checkIcon: UIImage = {
return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in
@ -41,17 +46,23 @@ final class GreetingMessageSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mode: GreetingMessageSetupScreen.Mode
init(
context: AccountContext
context: AccountContext,
mode: GreetingMessageSetupScreen.Mode
) {
self.context = context
self.mode = mode
}
static func ==(lhs: GreetingMessageSetupScreenComponent, rhs: GreetingMessageSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
@ -62,22 +73,6 @@ final class GreetingMessageSetupScreenComponent: Component {
}
}
private struct BotResolutionState: Equatable {
enum State: Equatable {
case searching
case notFound
case found(peer: EnginePeer, isInstalled: Bool)
}
var query: String
var state: State
init(query: String, state: State) {
self.query = query
self.state = state
}
}
private struct AdditionalPeerList {
enum Category: Int {
case newChats = 0
@ -105,6 +100,12 @@ final class GreetingMessageSetupScreenComponent: Component {
}
}
private enum Schedule {
case always
case outsideBusinessHours
case custom
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
@ -113,19 +114,27 @@ final class GreetingMessageSetupScreenComponent: Component {
private let icon = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let generalSection = ComponentView<Empty>()
private let messagesSection = ComponentView<Empty>()
private let scheduleSection = ComponentView<Empty>()
private let customScheduleSection = ComponentView<Empty>()
private let accessSection = ComponentView<Empty>()
private let excludedSection = ComponentView<Empty>()
private let permissionsSection = ComponentView<Empty>()
private let periodSection = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var isUpdating: Bool = false
private var component: GreetingMessageSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var chevronImage: UIImage?
private var isOn: Bool = false
private var accountPeer: EnginePeer?
private var messages: [EngineMessage] = []
private var schedule: Schedule = .always
private var customScheduleStart: Date?
private var customScheduleEnd: Date?
private var hasAccessToAllChatsByDefault: Bool = true
private var additionalPeerList = AdditionalPeerList(
@ -135,6 +144,8 @@ final class GreetingMessageSetupScreenComponent: Component {
private var replyToMessages: Bool = true
private var messagesDisposable: Disposable?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -161,6 +172,7 @@ final class GreetingMessageSetupScreenComponent: Component {
}
deinit {
self.messagesDisposable?.dispose()
}
func scrollToTop() {
@ -172,7 +184,9 @@ final class GreetingMessageSetupScreenComponent: Component {
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
var scrolledUp = true
@ -332,12 +346,140 @@ final class GreetingMessageSetupScreenComponent: Component {
self.environment?.controller()?.push(controller)
}
private func openMessageList() {
guard let component = self.component else {
return
}
let contents = GreetingMessageSetupChatContents(
context: component.context,
messages: self.messages,
kind: component.mode == .away ? .awayMessageInput : .greetingMessageInput
)
let chatController = component.context.sharedContext.makeChatController(
context: component.context,
chatLocation: .customChatContents,
subject: .customChatContents(contents: contents),
botStart: nil,
mode: .standard(.default)
)
chatController.navigationPresentation = .modal
self.environment?.controller()?.push(chatController)
self.messagesDisposable?.dispose()
self.messagesDisposable = (contents.messages
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
guard let self else {
return
}
let messages = messages.map(EngineMessage.init)
if self.messages != messages {
self.messages = messages
self.state?.updated(transition: .immediate)
}
})
}
private func openCustomScheduleDateSetup(isStartTime: Bool, isDate: Bool) {
guard let component = self.component else {
return
}
let currentValue: Date = (isStartTime ? self.customScheduleStart : self.customScheduleEnd) ?? Date()
if isDate {
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month, .day], from: currentValue)
guard let clippedDate = calendar.date(from: components) else {
return
}
let controller = DateSelectionActionSheetController(
context: component.context,
title: nil,
currentValue: Int32(clippedDate.timeIntervalSince1970),
minimumDate: nil,
maximumDate: nil,
emptyTitle: nil,
applyValue: { [weak self] value in
guard let self else {
return
}
guard let value else {
return
}
let updatedDate = Date(timeIntervalSince1970: Double(value))
let calendar = Calendar.current
var updatedComponents = calendar.dateComponents([.year, .month, .day], from: updatedDate)
let currentComponents = calendar.dateComponents([.hour, .minute], from: currentValue)
updatedComponents.hour = currentComponents.hour
updatedComponents.minute = currentComponents.minute
guard let updatedClippedDate = calendar.date(from: updatedComponents) else {
return
}
if isStartTime {
self.customScheduleStart = updatedClippedDate
} else {
self.customScheduleEnd = updatedClippedDate
}
self.state?.updated(transition: .immediate)
}
)
self.environment?.controller()?.present(controller, in: .window(.root))
} else {
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: currentValue)
let hour = components.hour ?? 0
let minute = components.minute ?? 0
let controller = TimeSelectionActionSheet(context: component.context, currentValue: Int32(hour * 60 * 60 + minute * 60), applyValue: { [weak self] value in
guard let self else {
return
}
guard let value else {
return
}
let updatedHour = value / (60 * 60)
let updatedMinute = (value % (60 * 60)) / 60
let calendar = Calendar.current
var updatedComponents = calendar.dateComponents([.year, .month, .day], from: currentValue)
updatedComponents.hour = Int(updatedHour)
updatedComponents.minute = Int(updatedMinute)
guard let updatedClippedDate = calendar.date(from: updatedComponents) else {
return
}
if isStartTime {
self.customScheduleStart = updatedClippedDate
} else {
self.customScheduleEnd = updatedClippedDate
}
self.state?.updated(transition: .immediate)
})
self.environment?.controller()?.present(controller, in: .window(.root))
}
}
func update(component: GreetingMessageSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self else {
return
}
self.accountPeer = peer
})
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
@ -357,7 +499,7 @@ final class GreetingMessageSetupScreenComponent: Component {
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Greeting Message", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
text: .plain(NSAttributedString(string: component.mode == .greeting ? "Greeting Message" : "Away Message", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
@ -387,7 +529,7 @@ final class GreetingMessageSetupScreenComponent: Component {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "HandWaveEmoji"),
content: LottieComponent.AppBundleContent(name: component.mode == .greeting ? "HandWaveEmoji" : "ZzzEmoji"),
loop: true
)),
environment: {},
@ -405,7 +547,7 @@ final class GreetingMessageSetupScreenComponent: Component {
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Greet customers when they message you the first time or after a period of no activity.", attributes: MarkdownAttributes(
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(component.mode == .greeting ? "Greet customers when they message you the first time or after a period of no activity." : "Automatically reply with a message when you are away.", attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
@ -413,12 +555,6 @@ final class GreetingMessageSetupScreenComponent: Component {
return ("URL", "")
}), textAlignment: .center
))
if self.chevronImage == nil {
self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight")
}
if let range = subtitleString.string.range(of: ">"), let chevronImage = self.chevronImage {
subtitleString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: subtitleString.string))
}
//TODO:localize
let subtitleSize = self.subtitle.update(
@ -463,7 +599,7 @@ final class GreetingMessageSetupScreenComponent: Component {
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Send Greeting Message",
string: component.mode == .greeting ? "Send Greeting Message" : "Send Away Message",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
@ -504,6 +640,279 @@ final class GreetingMessageSetupScreenComponent: Component {
var otherSectionsHeight: CGFloat = 0.0
//TODO:localize
var messagesSectionItems: [AnyComponentWithIdentity<Empty>] = []
if let topMessage = self.messages.first {
if let accountPeer = self.accountPeer {
messagesSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(GreetingMessageListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
accountPeer: accountPeer,
message: topMessage,
count: self.messages.count,
action: { [weak self] in
guard let self else {
return
}
self.openMessageList()
}
))))
}
} else {
messagesSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? "Create a Greeting Message" : "Create an Away Message",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/ComposeIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openMessageList()
}
))))
}
let messagesSectionSize = self.messagesSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.mode == .greeting ? (self.messages.count > 1 ? "GREETING MESSAGES" : "GREETING MESSAGE") : (self.messages.count > 1 ? "AWAY MESSAGES" : "AWAY MESSAGE"),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: messagesSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let messagesSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: messagesSectionSize)
if let messagesSectionView = self.messagesSection.view {
if messagesSectionView.superview == nil {
messagesSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(messagesSectionView)
}
transition.setFrame(view: messagesSectionView, frame: messagesSectionFrame)
alphaTransition.setAlpha(view: messagesSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += messagesSectionSize.height
otherSectionsHeight += sectionSpacing
if case .away = component.mode {
//TODO:localize
var scheduleSectionItems: [AnyComponentWithIdentity<Empty>] = []
for i in 0 ..< 3 {
let title: String
let schedule: Schedule
switch i {
case 0:
title = "Always Send"
schedule = .always
case 1:
title = "Outside of Business Hours"
schedule = .outsideBusinessHours
default:
title = "Custom Schedule"
schedule = .custom
}
let isSelected = self.schedule == schedule
scheduleSectionItems.append(AnyComponentWithIdentity(id: scheduleSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
image: checkIcon,
tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor,
contentMode: .center
))),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
if self.schedule != schedule {
self.schedule = schedule
self.state?.updated(transition: .immediate)
}
}
))))
}
let scheduleSectionSize = self.scheduleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "SCHEDULE",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: scheduleSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let scheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: scheduleSectionSize)
if let scheduleSectionView = self.scheduleSection.view {
if scheduleSectionView.superview == nil {
scheduleSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(scheduleSectionView)
}
transition.setFrame(view: scheduleSectionView, frame: scheduleSectionFrame)
alphaTransition.setAlpha(view: scheduleSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
otherSectionsHeight += scheduleSectionSize.height
otherSectionsHeight += sectionSpacing
var customScheduleSectionsHeight: CGFloat = 0.0
var customScheduleSectionItems: [AnyComponentWithIdentity<Empty>] = []
for i in 0 ..< 2 {
let title: String
let itemDate: Date?
let isStartTime: Bool
switch i {
case 0:
title = "Start Time"
itemDate = self.customScheduleStart
isStartTime = true
default:
title = "End Time"
itemDate = self.customScheduleEnd
isStartTime = false
}
var icon: ListActionItemComponent.Icon?
if let itemDate {
let calendar = Calendar.current
let hours = calendar.component(.hour, from: itemDate)
let minutes = calendar.component(.minute, from: itemDate)
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let timeText = stringForShortTimestamp(hours: Int32(hours), minutes: Int32(minutes), dateTimeFormat: presentationData.dateTimeFormat)
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .none
dateFormatter.dateStyle = .medium
let dateText = stringForCompactDate(timestamp: Int32(itemDate.timeIntervalSince1970), strings: environment.strings, dateTimeFormat: presentationData.dateTimeFormat)
icon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: dateText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true)
},
animateAlpha: true,
animateScale: false
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: timeText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: false)
},
animateAlpha: true,
animateScale: false
)))
], spacing: 4.0))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true)
}
customScheduleSectionItems.append(AnyComponentWithIdentity(id: customScheduleSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: icon,
accessory: nil,
action: itemDate != nil ? nil : { [weak self] _ in
guard let self else {
return
}
self.openCustomScheduleDateSetup(isStartTime: isStartTime, isDate: true)
}
))))
}
let customScheduleSectionSize = self.customScheduleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: customScheduleSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let customScheduleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight + customScheduleSectionsHeight), size: customScheduleSectionSize)
if let customScheduleSectionView = self.customScheduleSection.view {
if customScheduleSectionView.superview == nil {
customScheduleSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(customScheduleSectionView)
}
transition.setFrame(view: customScheduleSectionView, frame: customScheduleSectionFrame)
alphaTransition.setAlpha(view: customScheduleSectionView, alpha: (self.isOn && self.schedule == .custom) ? 1.0 : 0.0)
}
customScheduleSectionsHeight += customScheduleSectionSize.height
customScheduleSectionsHeight += sectionSpacing
if self.schedule == .custom {
otherSectionsHeight += customScheduleSectionsHeight
}
}
//TODO:localize
let accessSectionSize = self.accessSection.update(
transition: transition,
@ -700,7 +1109,7 @@ final class GreetingMessageSetupScreenComponent: Component {
)),
footer: AnyComponent(MultilineTextComponent(
text: .markdown(
text: self.hasAccessToAllChatsByDefault ? "Select chats or entire chat categories which the bot **WILL NOT** have access to." : "Select chats or entire chat categories which the bot **WILL** have access to.",
text: component.mode == .greeting ? "Choose chats or entire chat categories for sending a greeting message." : "Choose chats or entire chat categories for sending an away message.",
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
@ -729,65 +1138,61 @@ final class GreetingMessageSetupScreenComponent: Component {
otherSectionsHeight += excludedSectionSize.height
otherSectionsHeight += sectionSpacing
//TODO:localize
/*let permissionsSectionSize = self.permissionsSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "BOT PERMISSIONS",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
if case .greeting = component.mode {
let periodSectionSize = self.periodSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "PERIOD OF NO ACTIVITY",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Choose how many days should pass after your last interaction with a recipient to send them the greeting in response to their message.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Reply to Messages",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .icons, isOn: self.replyToMessages, action: { [weak self] _ in
guard let self else {
return
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
theme: environment.theme,
values: [
"7 days",
"14 days",
"21 days",
"28 days"
],
selectedIndex: 0,
selectedIndexUpdated: { [weak self] index in
guard let self else {
return
}
let _ = self
}
self.replyToMessages = !self.replyToMessages
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let permissionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: permissionsSectionSize)
if let permissionsSectionView = self.permissionsSection.view {
if permissionsSectionView.superview == nil {
permissionsSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(permissionsSectionView)
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let periodSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: periodSectionSize)
if let periodSectionView = self.periodSection.view {
if periodSectionView.superview == nil {
periodSectionView.layer.allowsGroupOpacity = true
self.scrollView.addSubview(periodSectionView)
}
transition.setFrame(view: periodSectionView, frame: periodSectionFrame)
alphaTransition.setAlpha(view: periodSectionView, alpha: self.isOn ? 1.0 : 0.0)
}
transition.setFrame(view: permissionsSectionView, frame: permissionsSectionFrame)
alphaTransition.setAlpha(view: permissionsSectionView, alpha: self.isOn ? 1.0 : 0.0)
otherSectionsHeight += periodSectionSize.height
otherSectionsHeight += sectionSpacing
}
otherSectionsHeight += permissionsSectionSize.height*/
if self.isOn {
contentHeight += otherSectionsHeight
@ -799,6 +1204,7 @@ final class GreetingMessageSetupScreenComponent: Component {
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
self.ignoreScrolling = true
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
@ -809,6 +1215,7 @@ final class GreetingMessageSetupScreenComponent: Component {
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
self.ignoreScrolling = false
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
@ -836,13 +1243,19 @@ final class GreetingMessageSetupScreenComponent: Component {
}
public final class GreetingMessageSetupScreen: ViewControllerComponentContainer {
public enum Mode {
case greeting
case away
}
private let context: AccountContext
public init(context: AccountContext) {
public init(context: AccountContext, mode: Mode) {
self.context = context
super.init(context: context, component: GreetingMessageSetupScreenComponent(
context: context
context: context,
mode: mode
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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",
],
)

View File

@ -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)
}
}
}
}

View File

@ -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 })
}
}

View File

@ -13,9 +13,8 @@ swift_library(
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/LegacyUI",
"//submodules/LegacyComponents",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -3,96 +3,69 @@ import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import LegacyUI
import LegacyComponents
import ComponentFlow
import MultilineTextComponent
final class SliderComponent: Component {
typealias EnvironmentType = Empty
public final class SliderComponent: Component {
public let valueCount: Int
public let value: Int
public let trackBackgroundColor: UIColor
public let trackForegroundColor: UIColor
public let valueUpdated: (Int) -> Void
public let isTrackingUpdated: ((Bool) -> Void)?
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
let isEnabled: Bool
let trackColor: UIColor?
let displayValue: Bool
let valueUpdated: (Float) -> Void
let isTrackingUpdated: ((Bool) -> Void)?
init(
title: String,
value: Float,
minValue: Float,
maxValue: Float,
startValue: Float,
isEnabled: Bool,
trackColor: UIColor?,
displayValue: Bool,
valueUpdated: @escaping (Float) -> Void,
public init(
valueCount: Int,
value: Int,
trackBackgroundColor: UIColor,
trackForegroundColor: UIColor,
valueUpdated: @escaping (Int) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil
) {
self.title = title
self.valueCount = valueCount
self.value = value
self.minValue = minValue
self.maxValue = maxValue
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.trackBackgroundColor = trackBackgroundColor
self.trackForegroundColor = trackForegroundColor
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool {
if lhs.title != rhs.title {
public static func ==(lhs: SliderComponent, rhs: SliderComponent) -> Bool {
if lhs.valueCount != rhs.valueCount {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.minValue != rhs.minValue {
if lhs.trackBackgroundColor != rhs.trackBackgroundColor {
return false
}
if lhs.maxValue != rhs.maxValue {
return false
}
if lhs.startValue != rhs.startValue {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.trackColor != rhs.trackColor {
return false
}
if lhs.displayValue != rhs.displayValue {
if lhs.trackForegroundColor != rhs.trackForegroundColor {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let title = ComponentView<Empty>()
private let value = ComponentView<Empty>()
public final class View: UIView {
private var sliderView: TGPhotoEditorSliderView?
private var component: SliderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
override public init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
func update(component: SliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: availableSize.width, height: 44.0)
var internalIsTrackingUpdated: ((Bool) -> Void)?
if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in
@ -100,23 +73,11 @@ final class SliderComponent: Component {
if isTracking {
self.sliderView?.bordered = true
} else {
Queue.mainQueue().after(0.1) {
self.sliderView?.bordered = false
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
self?.sliderView?.bordered = false
})
}
isTrackingUpdated(isTracking)
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
}
if let valueView = self.value.view {
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
@ -124,24 +85,42 @@ final class SliderComponent: Component {
let sliderView: TGPhotoEditorSliderView
if let current = self.sliderView {
sliderView = current
sliderView.value = CGFloat(component.value)
} else {
sliderView = TGPhotoEditorSliderView()
sliderView.backgroundColor = .clear
sliderView.startColor = UIColor(rgb: 0xffffff)
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = CGFloat(component.minValue)
sliderView.maximumValue = CGFloat(component.maxValue)
sliderView.startValue = CGFloat(component.startValue)
sliderView.value = CGFloat(component.value)
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.maximumValue = CGFloat(component.valueCount)
sliderView.positionsCount = component.valueCount + 1
sliderView.useLinesForPositions = true
sliderView.backgroundColor = nil
sliderView.isOpaque = false
sliderView.backColor = component.trackBackgroundColor
sliderView.startColor = component.trackBackgroundColor
sliderView.trackColor = component.trackForegroundColor
sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
sliderView.layer.allowsGroupOpacity = true
self.sliderView = sliderView
self.addSubview(sliderView)
}
sliderView.value = CGFloat(component.value)
sliderView.interactionBegan = {
internalIsTrackingUpdated?(true)
}
@ -149,70 +128,17 @@ final class SliderComponent: Component {
internalIsTrackingUpdated?(false)
}
if component.isEnabled {
sliderView.alpha = 1.3
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
sliderView.isUserInteractionEnabled = true
} else {
sliderView.trackColor = UIColor(rgb: 0xffffff)
sliderView.alpha = 0.3
sliderView.isUserInteractionEnabled = false
}
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: 44.0)))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
}
let valueText: String
if component.displayValue {
if component.value > 0.005 {
valueText = String(format: "+%.2f", component.value)
} else if component.value < -0.005 {
valueText = String(format: "%.2f", component.value)
} else {
valueText = ""
}
} else {
valueText = ""
}
let valueSize = self.value.update(
transition: .immediate,
component: AnyComponent(
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xf8d74a))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let valueView = self.value.view {
if valueView.superview == nil {
self.addSubview(valueView)
}
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
}
return CGSize(width: availableSize.width, height: 52.0)
return size
}
@objc private func sliderValueChanged() {
guard let component = self.component, let sliderView = self.sliderView else {
return
}
component.valueUpdated(Float(sliderView.value))
component.valueUpdated(Int(sliderView.value))
}
}
@ -220,240 +146,7 @@ final class SliderComponent: Component {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
struct AdjustmentTool: Equatable {
let key: EditorToolKey
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
}
final class AdjustmentsComponent: Component {
typealias EnvironmentType = Empty
let tools: [AdjustmentTool]
let valueUpdated: (EditorToolKey, Float) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
tools: [AdjustmentTool],
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.tools = tools
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
if lhs.tools != rhs.tools {
return false
}
return true
}
final class View: UIView {
private let scrollView = UIScrollView()
private var toolViews: [ComponentView<Empty>] = []
private var component: AdjustmentsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let valueUpdated = component.valueUpdated
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
component.isTrackingUpdated(isTracking)
if let self {
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
if tool.key != trackingTool && i < self.toolViews.count {
if let view = self.toolViews[i].view {
let transition: Transition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
}
var sizes: [CGSize] = []
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
let componentView: ComponentView<Empty>
if i >= self.toolViews.count {
componentView = ComponentView<Empty>()
self.toolViews.append(componentView)
} else {
componentView = self.toolViews[i]
}
var valueIsNegative = false
var value = tool.value
if case .enhance = tool.key {
if value < 0.0 {
valueIsNegative = true
}
value = abs(value)
}
let size = componentView.update(
transition: transition,
component: AnyComponent(
SliderComponent(
title: tool.title,
value: value,
minValue: tool.minValue,
maxValue: tool.maxValue,
startValue: tool.startValue,
isEnabled: true,
trackColor: nil,
displayValue: true,
valueUpdated: { value in
var updatedValue = value
if valueIsNegative {
updatedValue *= -1.0
}
valueUpdated(tool.key, updatedValue)
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(tool.key, isTracking)
}
)
),
environment: {},
containerSize: availableSize
)
sizes.append(size)
}
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
for i in 0 ..< component.tools.count {
let size = sizes[i]
let componentView = self.toolViews[i]
if let view = componentView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
}
origin = origin.offsetBy(dx: 0.0, dy: size.height)
}
let size = CGSize(width: availableSize.width, height: 180.0)
let contentSize = CGSize(width: availableSize.width, height: origin.y)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class AdjustmentsScreenComponent: Component {
typealias EnvironmentType = Empty
let toggleUneditedPreview: (Bool) -> Void
init(
toggleUneditedPreview: @escaping (Bool) -> Void
) {
self.toggleUneditedPreview = toggleUneditedPreview
}
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var component: AdjustmentsScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.05
self.addGestureRecognizer(longPressGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.toggleUneditedPreview(true)
case .ended, .cancelled:
component.toggleUneditedPreview(false)
default:
break
}
}
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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",
],
)

View File

@ -9,15 +9,15 @@ import TelegramStringFormatting
import AccountContext
import UIKitRuntimeUtils
final class TimeSelectionActionSheet: ActionSheetController {
public final class TimeSelectionActionSheet: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
override public var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
public init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
@ -56,7 +56,7 @@ final class TimeSelectionActionSheet: ActionSheetController {
])
}
required init(coder aDecoder: NSCoder) {
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

View File

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

View File

@ -289,7 +289,7 @@ extension ChatControllerImpl {
if let location = location {
source = .location(ChatMessageContextLocationContentSource(controller: self, location: node.view.convert(node.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y)))
} else {
source = .extracted(ChatMessageContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll))
source = .extracted(ChatMessageContextExtractedContentSource(chatController: self, chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: selectAll))
}
self.canReadHistory.set(false)

View File

@ -137,7 +137,7 @@ public final class ChatControllerOverlayPresentationData {
enum ChatLocationInfoData {
case peer(Promise<PeerView>)
case replyThread(Promise<Message?>)
case feed
case customChatContents
}
enum ChatRecordingActivity {
@ -647,10 +647,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
promise.set(.single(nil))
}
self.chatLocationInfoData = .replyThread(promise)
case .feed:
case .customChatContents:
locationBroadcastPanelSource = .none
groupCallPanelSource = .none
self.chatLocationInfoData = .feed
self.chatLocationInfoData = .customChatContents
}
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -2296,7 +2296,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
case .replyThread:
postAsReply = true
case .feed:
case .customChatContents:
postAsReply = true
}
@ -2902,7 +2902,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .replyThread(replyThreadMessage):
let peerId = replyThreadMessage.peerId
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil)
case .feed:
case .customChatContents:
break
}
}, requestRedeliveryOfFailedMessages: { [weak self] id in
@ -2943,7 +2943,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: message._asMessage(), selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
strongSelf.currentContextController = controller
strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen {
@ -3021,7 +3021,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatController: strongSelf, chatNode: strongSelf.chatDisplayNode, engine: strongSelf.context.engine, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil)
strongSelf.currentContextController = controller
strongSelf.forEachController({ controller in
if let controller = controller as? TooltipScreen {
@ -4787,7 +4787,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode
case .feed:
case .customChatContents:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
chatInfoButtonItem.target = self
@ -5669,7 +5669,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
replyThreadType = .replies
}
}
case .feed:
case .customChatContents:
replyThreadType = .replies
}
@ -6149,11 +6149,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}))
} else if case .feed = self.chatLocationInfoData {
} else if case .customChatContents = self.chatLocationInfoData {
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
self.titleDisposable.set(nil)
self.chatTitleView?.titleContent = .custom("Feed", nil, false)
//TODO:localize
if case let .customChatContents(customChatContents) = self.subject {
switch customChatContents.kind {
case .greetingMessageInput:
self.chatTitleView?.titleContent = .custom("Greeting Message", nil, false)
case .awayMessageInput:
self.chatTitleView?.titleContent = .custom("Away Message", nil, false)
case let .quickReplyMessageInput(shortcut):
self.chatTitleView?.titleContent = .custom("/\(shortcut)", nil, false)
}
} else {
self.chatTitleView?.titleContent = .custom("Messages", nil, false)
}
if !self.didSetChatLocationInfoReady {
self.didSetChatLocationInfoReady = true
@ -6307,7 +6319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .feed:
case .customChatContents:
activitySpace = nil
}
@ -7763,7 +7775,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .peer:
pinnedMessageId = topPinnedMessage?.message.id
pinnedMessage = topPinnedMessage
case .feed:
case .customChatContents:
pinnedMessageId = nil
pinnedMessage = nil
}
@ -7934,7 +7946,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
$0.updatedChatHistoryState(state)
})
if let botStart = strongSelf.botStart, case let .loaded(isEmpty) = state {
if let botStart = strongSelf.botStart, case let .loaded(isEmpty, _) = state {
strongSelf.botStart = nil
if !isEmpty {
strongSelf.startBot(botStart.payload)
@ -8171,20 +8183,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in
if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId {
var correlationIds: [Int64] = []
for message in messages {
switch message {
case let .message(_, _, _, _, _, _, _, _, correlationId, _):
if let correlationId = correlationId {
correlationIds.append(correlationId)
}
default:
break
guard let strongSelf = self else {
return
}
var correlationIds: [Int64] = []
for message in messages {
switch message {
case let .message(_, _, _, _, _, _, _, _, correlationId, _):
if let correlationId = correlationId {
correlationIds.append(correlationId)
}
default:
break
}
strongSelf.commitPurposefulAction()
}
strongSelf.commitPurposefulAction()
if let peerId = strongSelf.chatLocation.peerId {
var hasDisabledContent = false
if "".isEmpty {
hasDisabledContent = false
@ -8271,9 +8287,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId])
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
} else if case let .customChatContents(customChatContents) = strongSelf.subject {
customChatContents.enqueueMessages(messages: messages)
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
}
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in
@ -9066,7 +9085,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId))
let sourceMessage: Signal<EngineMessage?, NoError>
if case let .customChatContents(customChatContents) = strongSelf.subject {
sourceMessage = customChatContents.messages
|> take(1)
|> map { messages -> EngineMessage? in
return messages.first(where: { $0.id == editMessage.messageId }).flatMap(EngineMessage.init)
}
} else {
sourceMessage = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId))
}
let _ = (sourceMessage
|> deliverOnMainQueue).start(next: { [weak strongSelf] message in
guard let strongSelf, let message else {
return
@ -9161,27 +9191,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
media = .keep
}
let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId)
|> deliverOnMainQueue)
.startStandalone(next: { [weak self] currentMessage in
if let strongSelf = self {
if let currentMessage = currentMessage {
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? []
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false)
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview {
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
if case let .customChatContents(customChatContents) = strongSelf.subject {
customChatContents.editMessage(id: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var state = state
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
state = state.updatedEditMessageState(nil)
return state
})
} else {
let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId)
|> deliverOnMainQueue).startStandalone(next: { [weak self] currentMessage in
if let strongSelf = self {
if let currentMessage = currentMessage {
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? []
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false)
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview {
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
}
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var state = state
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
state = state.updatedEditMessageState(nil)
return state
})
}
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var state = state
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
state = state.updatedEditMessageState(nil)
return state
})
}
})
})
}
})
}, beginMessageSearch: { [weak self] domain, query in
guard let strongSelf = self else {
@ -9313,7 +9353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.updateItemNodesSearchTextHighlightStates()
if let navigateIndex = navigateIndex {
switch strongSelf.chatLocation {
case .peer, .replyThread, .feed:
case .peer, .replyThread, .customChatContents:
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
}
}
@ -11249,7 +11289,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .feed:
case .customChatContents:
activitySpace = nil
}
@ -12152,7 +12192,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
peerId = replyThreadMessage.peerId
threadId = replyThreadMessage.threadId
}
case .feed:
case .customChatContents:
return
}
@ -12710,7 +12750,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.effectiveNavigationController?.pushViewController(infoController)
}
}
case .feed:
case .customChatContents:
break
}
})
@ -12922,7 +12962,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
case .replyThread:
break
case .feed:
case .customChatContents:
break
}
}
@ -13654,7 +13694,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let effectiveMessageId = replyThreadMessage.effectiveMessageId {
defaultReplyMessageSubject = EngineMessageReplySubject(messageId: effectiveMessageId, quote: nil)
}
case .feed:
case .customChatContents:
break
}
@ -13709,6 +13749,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) {
if case let .customChatContents(customChatContents) = self.subject {
customChatContents.enqueueMessages(messages: messages)
return
}
guard let peerId = self.chatLocation.peerId else {
return
}

View File

@ -588,6 +588,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, loadMore: nil)
}
} else if case .customChatContents = chatLocation {
if case let .customChatContents(customChatContents) = subject {
source = .custom(
messages: customChatContents.messages
|> map { messages in
return (messages, 0, false)
},
messageId: nil,
quote: nil,
loadMore: nil
)
} else {
source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, loadMore: nil)
}
} else {
source = .default
}

View File

@ -39,10 +39,6 @@ extension ChatControllerImpl {
}
func presentAttachmentMenu(subject: AttachMenuSubject) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
let context = self.context
let inputIsActive = self.presentationInterfaceState.inputMode == .text
@ -56,42 +52,46 @@ extension ChatControllerImpl {
var bannedSendFiles: (Int32, Bool)?
var canSendPolls = true
if let peer = peer as? TelegramUser, peer.botInfo == nil {
canSendPolls = false
} else if peer is TelegramSecretChat {
canSendPolls = false
} else if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos, ignoreDefault: canByPassRestrictions) {
bannedSendVideos = value
}
if let value = channel.hasBannedPermission(.banSendFiles, ignoreDefault: canByPassRestrictions) {
bannedSendFiles = value
}
if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) {
banSendText = value
}
if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil {
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
if let peer = peer as? TelegramUser, peer.botInfo == nil {
canSendPolls = false
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendFiles) {
bannedSendFiles = (Int32.max, false)
}
if group.hasBannedPermission(.banSendText) {
banSendText = (Int32.max, false)
}
if group.hasBannedPermission(.banSendPolls) {
} else if peer is TelegramSecretChat {
canSendPolls = false
} else if let channel = peer as? TelegramChannel {
if let value = channel.hasBannedPermission(.banSendPhotos, ignoreDefault: canByPassRestrictions) {
bannedSendPhotos = value
}
if let value = channel.hasBannedPermission(.banSendVideos, ignoreDefault: canByPassRestrictions) {
bannedSendVideos = value
}
if let value = channel.hasBannedPermission(.banSendFiles, ignoreDefault: canByPassRestrictions) {
bannedSendFiles = value
}
if let value = channel.hasBannedPermission(.banSendText, ignoreDefault: canByPassRestrictions) {
banSendText = value
}
if channel.hasBannedPermission(.banSendPolls, ignoreDefault: canByPassRestrictions) != nil {
canSendPolls = false
}
} else if let group = peer as? TelegramGroup {
if group.hasBannedPermission(.banSendPhotos) {
bannedSendPhotos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendVideos) {
bannedSendVideos = (Int32.max, false)
}
if group.hasBannedPermission(.banSendFiles) {
bannedSendFiles = (Int32.max, false)
}
if group.hasBannedPermission(.banSendText) {
banSendText = (Int32.max, false)
}
if group.hasBannedPermission(.banSendPolls) {
canSendPolls = false
}
}
} else {
canSendPolls = false
}
var availableButtons: [AttachmentButtonType] = [.gallery, .file]
@ -111,24 +111,26 @@ extension ChatControllerImpl {
}
var peerType: AttachMenuBots.Bot.PeerFlags = []
if let user = peer as? TelegramUser {
if let _ = user.botInfo {
peerType.insert(.bot)
} else {
peerType.insert(.user)
}
} else if let _ = peer as? TelegramGroup {
peerType = .group
} else if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
peerType = .channel
} else {
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
if let user = peer as? TelegramUser {
if let _ = user.botInfo {
peerType.insert(.bot)
} else {
peerType.insert(.user)
}
} else if let _ = peer as? TelegramGroup {
peerType = .group
} else if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
peerType = .channel
} else {
peerType = .group
}
}
}
let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError>
if !isScheduledMessages && !peer.isDeleted {
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted {
buttons = self.context.engine.messages.attachMenuBots()
|> map { attachMenuBots in
var buttons = availableButtons
@ -177,7 +179,7 @@ extension ChatControllerImpl {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let premiumGiftOptions: [CachedPremiumGiftOption]
if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) {
if let peer = self.presentationInterfaceState.renderedPeer?.peer, !premiumConfiguration.isPremiumDisabled, premiumConfiguration.showPremiumGiftInAttachMenu, let user = peer as? TelegramUser, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) {
premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions
} else {
premiumGiftOptions = []
@ -324,20 +326,30 @@ extension ChatControllerImpl {
return
}
let selfPeerId: PeerId
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
selfPeerId = peer.id
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
selfPeerId = peer.id
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
selfPeerId = peer.id
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
selfPeerId = peer.id
} else {
selfPeerId = strongSelf.context.account.peerId
}
} else {
selfPeerId = strongSelf.context.account.peerId
}
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfPeerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] selfPeer in
|> deliverOnMainQueue).startStandalone(next: { selfPeer in
guard let strongSelf = self, let selfPeer = selfPeer else {
return
}
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in
let hasLiveLocation: Bool
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
} else {
hasLiveLocation = false
}
let sharePeer = (strongSelf.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init)
let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: sharePeer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { location, _, _, _, _ in
guard let strongSelf = self else {
return
}
@ -523,69 +535,73 @@ extension ChatControllerImpl {
completion(controller, controller?.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
case .gift:
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
if !premiumGiftOptions.isEmpty {
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
if !premiumGiftOptions.isEmpty {
let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in
if let strongSelf = self {
strongSelf.push(c)
}
}, completion: { [weak self] in
if let strongSelf = self {
strongSelf.hintPlayNextOutgoingGift()
strongSelf.attachmentController?.dismiss(animated: true)
}
})
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone()
}
}
case let .app(bot):
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
var payload: String?
var fromAttachMenu = true
if case let .bot(_, botPayload, _) = subject {
payload = botPayload
fromAttachMenu = false
}
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false)
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId)
controller.openUrl = { [weak self] url, concealed, commit in
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
}
controller.getNavigationController = { [weak self] in
return self?.effectiveNavigationController
}
controller.completion = { [weak self] in
if let strongSelf = self {
strongSelf.push(c)
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
})
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
}, completion: { [weak self] in
if let strongSelf = self {
strongSelf.hintPlayNextOutgoingGift()
strongSelf.attachmentController?.dismiss(animated: true)
}
})
}
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).startStandalone()
}
case let .app(bot):
var payload: String?
var fromAttachMenu = true
if case let .bot(_, botPayload, _) = subject {
payload = botPayload
fromAttachMenu = false
}
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false)
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId)
controller.openUrl = { [weak self] url, concealed, commit in
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
}
controller.getNavigationController = { [weak self] in
return self?.effectiveNavigationController
}
controller.completion = { [weak self] in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
if bot.flags.contains(.notActivated) {
let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
guard let self else {
return
}
if bot.flags.contains(.showInSettingsDisclaimer) {
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
}
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|> deliverOnMainQueue).startStandalone(error: { _ in
}, completed: { [weak controller] in
controller?.refresh()
})
},
dismissed: {
strongSelf.attachmentController?.dismiss(animated: true)
})
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
strongSelf.present(alertController, in: .window(.root))
}
}
completion(controller, controller.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set(nil)
if bot.flags.contains(.notActivated) {
let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
guard let self else {
return
}
if bot.flags.contains(.showInSettingsDisclaimer) {
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).startStandalone()
}
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|> deliverOnMainQueue).startStandalone(error: { _ in
}, completed: { [weak controller] in
controller?.refresh()
})
},
dismissed: {
strongSelf.attachmentController?.dismiss(animated: true)
})
strongSelf.present(alertController, in: .window(.root))
}
default:
break
}
@ -1063,9 +1079,6 @@ extension ChatControllerImpl {
}
func presentMediaPicker(subject: MediaPickerScreen.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
var isScheduledMessages = false
if case .scheduledMessages = self.presentationInterfaceState.subject {
isScheduledMessages = true
@ -1073,7 +1086,7 @@ extension ChatControllerImpl {
let controller = MediaPickerScreen(
context: self.context,
updatedPresentationData: self.updatedPresentationData,
peer: EnginePeer(peer),
peer: (self.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init),
threadTitle: self.threadInfo?.title,
chatLocation: self.chatLocation,
isScheduledMessages: isScheduledMessages,

View File

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

View File

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

View File

@ -53,7 +53,7 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
text = interfaceState.strings.ChatList_StartMessaging
} else {
switch interfaceState.chatLocation {
case .peer, .replyThread, .feed:
case .peer, .replyThread, .customChatContents:
if case .scheduledMessages = interfaceState.subject {
text = interfaceState.strings.ScheduledMessages_EmptyPlaceholder
} else {
@ -701,6 +701,12 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
}
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var maxWidth: CGFloat = size.width
var centerText = false
if case .customChatContents = interfaceState.subject {
maxWidth = min(240.0, maxWidth)
}
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings
@ -709,17 +715,56 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: serviceColor.primaryText)
let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title
let titleString: String
let strings: [String]
if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .greetingMessageInput:
//TODO:localize
centerText = true
titleString = "New Greeting Message"
strings = [
"Create greetings that will be automatically sent to new customers"
]
case .awayMessageInput:
//TODO:localize
centerText = true
titleString = "New Away Message"
strings = [
"Add messages that are automatically sent when you are off."
]
case let .quickReplyMessageInput(shortcut):
//TODO:localize
centerText = false
titleString = "New Quick Reply"
strings = [
"Enter a message below that will be sent in chats when you type \"**/\(shortcut)\"**.",
"You can access Quick Replies in any chat by typing \"/\" or using the Attachment menu."
]
}
} else {
titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title
strings = [
interfaceState.strings.Conversation_ClousStorageInfo_Description1,
interfaceState.strings.Conversation_ClousStorageInfo_Description2,
interfaceState.strings.Conversation_ClousStorageInfo_Description3,
interfaceState.strings.Conversation_ClousStorageInfo_Description4
]
}
self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText)
let strings: [String] = [
interfaceState.strings.Conversation_ClousStorageInfo_Description1,
interfaceState.strings.Conversation_ClousStorageInfo_Description2,
interfaceState.strings.Conversation_ClousStorageInfo_Description3,
interfaceState.strings.Conversation_ClousStorageInfo_Description4
]
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) }
let lines: [NSAttributedString] = strings.map {
return parseMarkdownIntoAttributedString($0, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText),
bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: serviceColor.primaryText),
link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: centerText ? .center : .natural)
}
for i in 0 ..< lines.count {
if i >= self.lineNodes.count {
@ -727,6 +772,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
textNode.maximumNumberOfLines = 0
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
textNode.textAlignment = centerText ? .center : .natural
self.addSubnode(textNode)
self.lineNodes.append(textNode)
}
@ -751,7 +797,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
var lineNodes: [(CGSize, ImmediateTextNode)] = []
for textNode in self.lineNodes {
let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude))
let textSize = textNode.updateLayout(CGSize(width: maxWidth - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude))
contentWidth = max(contentWidth, textSize.width)
contentHeight += textSize.height + titleSpacing
lineNodes.append((textSize, textNode))
@ -1166,7 +1212,9 @@ final class ChatEmptyNode: ASDisplayNode {
case .detailsPlaceholder:
contentType = .regular
case let .emptyChat(emptyType):
if case .replyThread = interfaceState.chatLocation {
if case .customChatContents = interfaceState.subject {
contentType = .cloud
} else if case .replyThread = interfaceState.chatLocation {
if case .topic = emptyType {
contentType = .topic
} else {

View File

@ -212,13 +212,18 @@ extension ListMessageItemInteraction {
}
private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] {
var disableFloatingDateHeaders = false
if case .customChatContents = chatLocation {
disableFloatingDateHeaders = true
}
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
let item: ListViewItem
switch mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool
switch displayHeaders {
@ -236,7 +241,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
let item: ListViewItem
switch mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages))
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
case .list:
assertionFailure()
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
@ -257,13 +262,18 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
}
private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
var disableFloatingDateHeaders = false
if case .customChatContents = chatLocation {
disableFloatingDateHeaders = true
}
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
let item: ListViewItem
switch mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool
switch displayHeaders {
@ -281,7 +291,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
let item: ListViewItem
switch mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages))
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
case .list:
assertionFailure()
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
@ -2011,10 +2021,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
if apply {
switch strongSelf.chatLocation {
case .peer, .replyThread, .feed:
case .peer, .replyThread:
if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex)
}
case .customChatContents:
break
}
}
}).strict())
@ -2757,7 +2769,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
switch self.chatLocation {
case .peer:
messageIndex = maxIncomingIndex
case .replyThread, .feed:
case .replyThread, .customChatContents:
messageIndex = maxOverallIndex
}
@ -3142,7 +3154,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty)
var hasReachedLimits = false
if case let .customChatContents(customChatContents) = self.subject, let messageLimit = customChatContents.messageLimit {
hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit
}
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits)
if self.currentHistoryState != historyState {
self.currentHistoryState = historyState
self.historyState.set(historyState)
@ -3403,7 +3421,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
strongSelf.historyView = transition.historyView
let loadState: ChatHistoryNodeLoadState
var alwaysHasMessages = false
if case .custom = strongSelf.source {
if case .customChatContents = strongSelf.chatLocation {
} else {
alwaysHasMessages = true
}
}
if alwaysHasMessages {
loadState = .messages
} else if let historyView = strongSelf.historyView {
if historyView.filteredEntries.isEmpty {
@ -3513,7 +3538,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
switch strongSelf.chatLocation {
case .peer:
messageIndex = incomingIndex
case .replyThread, .feed:
case .replyThread, .customChatContents:
messageIndex = overallIndex
}
@ -3534,7 +3559,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty)
var hasReachedLimits = false
if case let .customChatContents(customChatContents) = strongSelf.subject, let messageLimit = customChatContents.messageLimit {
hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit
}
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits)
if strongSelf.currentHistoryState != historyState {
strongSelf.currentHistoryState = historyState
strongSelf.historyState.set(historyState)
@ -4027,6 +4056,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if let messageItem = messageItem {
let associatedData = messageItem.associatedData
let disableFloatingDateHeaders = messageItem.disableDate
loop: for i in 0 ..< historyView.filteredEntries.count {
switch historyView.filteredEntries[i] {
@ -4036,7 +4066,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let item: ListViewItem
switch self.mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool
switch displayHeaders {
@ -4083,6 +4113,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if let messageItem = messageItem {
let associatedData = messageItem.associatedData
let disableFloatingDateHeaders = messageItem.disableDate
loop: for i in 0 ..< historyView.filteredEntries.count {
switch historyView.filteredEntries[i] {
@ -4092,7 +4123,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let item: ListViewItem
switch self.mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location))
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool
switch displayHeaders {

View File

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

View File

@ -337,7 +337,7 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
}
case .replyThread:
canReply = true
case .feed:
case .customChatContents:
canReply = false
}
return canReply
@ -597,6 +597,68 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
return .single(ContextController.Items(content: .list(actions)))
}
if let message = messages.first, case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
var actions: [ContextMenuItem] = []
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
var messageEntities: [MessageTextEntity]?
var restrictedText: String?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
if let attribute = attribute as? RestrictedContentMessageAttribute {
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
}
}
if let restrictedText = restrictedText {
storeMessageTextInPasteboard(restrictedText, entities: nil)
} else {
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
} else {
storeMessageTextInPasteboard(message.text, entities: messageEntities)
}
}
Queue.mainQueue().after(0.2, {
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
controllerInteraction.displayUndo(content)
})
f(.default)
})))
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak customChatContents] _, f in
f(.dismissWithoutContent)
guard let customChatContents else {
return
}
customChatContents.deleteMessages(ids: messages.map(\.id))
})))
}
return .single(ContextController.Items(content: .list(actions)))
}
var loadStickerSaveStatus: MediaId?
var loadCopyMediaResource: MediaResource?
var isAction = false
@ -654,7 +716,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
canPin = false
} else if messages[0].flags.intersection([.Failed, .Unsent]).isEmpty {
switch chatPresentationInterfaceState.chatLocation {
case .peer, .replyThread, .feed:
case .peer, .replyThread, .customChatContents:
if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel {
if !isAction {
canPin = channel.hasPermission(.pinMessages)

View File

@ -368,7 +368,7 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartPanel = true
}
} else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true) = chatHistoryState {
} else if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState {
if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
displayBotStartPanel = true
}
@ -401,6 +401,24 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
}
}
if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject {
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
displayInputTextPanel = true
}
if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(_, true) = chatHistoryState {
if let currentPanel = (currentPanel as? ChatRestrictedInputPanelNode) ?? (currentSecondaryPanel as? ChatRestrictedInputPanelNode) {
return (currentPanel, nil)
} else {
let panel = ChatRestrictedInputPanelNode()
panel.context = context
panel.interfaceInteraction = interfaceInteraction
return (panel, nil)
}
}
}
if case .inline = chatPresentationInterfaceState.mode {
displayInputTextPanel = false
}

View File

@ -54,6 +54,15 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha
}
}
}
if case .customChatContents = presentationInterfaceState.subject {
if case .spacer = currentButton?.action {
return currentButton
} else {
return ChatNavigationButton(action: .spacer, buttonItem: UIBarButtonItem(title: " ", style: .plain, target: nil, action: nil))
}
}
return nil
}
@ -95,7 +104,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
var hasMessages = false
if let chatHistoryState = presentationInterfaceState.chatHistoryState {
if case .loaded(false) = chatHistoryState {
if case .loaded(false, _) = chatHistoryState {
hasMessages = true
}
}
@ -108,6 +117,16 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
return nil
}
if case .customChatContents = presentationInterfaceState.subject {
if let currentButton = currentButton, currentButton.action == .dismiss {
return currentButton
} else {
let buttonItem = UIBarButtonItem(title: strings.Common_Done, style: .done, target: target, action: selector)
buttonItem.accessibilityLabel = strings.Common_Done
return ChatNavigationButton(action: .dismiss, buttonItem: buttonItem)
}
}
if case .replyThread = presentationInterfaceState.chatLocation {
if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
} else if hasMessages {

View File

@ -34,6 +34,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
let blurBackground: Bool = true
let centerVertically: Bool
private weak var chatController: ChatControllerImpl?
private weak var chatNode: ChatControllerNode?
private let engine: TelegramEngine
private let message: Message
@ -43,6 +44,9 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
if self.message.adAttribute != nil {
return .single(false)
}
if let chatController = self.chatController, case .customChatContents = chatController.subject {
return .single(false)
}
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: self.message.id))
|> map { message -> Bool in
@ -55,7 +59,8 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|> distinctUntilChanged
}
init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
self.chatController = chatController
self.chatNode = chatNode
self.engine = engine
self.message = message

View File

@ -101,6 +101,14 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_DefaultRestrictedText, font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
}
}
} else if case let .customChatContents(customChatContents) = interfaceState.subject {
let displayCount: Int
switch customChatContents.kind {
case .greetingMessageInput, .awayMessageInput, .quickReplyMessageInput:
displayCount = 20
}
//TODO:localize
self.textNode.attributedText = NSAttributedString(string: "Limit of \(displayCount) messages reached", font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
}
self.buttonNode.isUserInteractionEnabled = isUserInteractionEnabled

View File

@ -34,7 +34,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern)
let placeholderText: String
switch chatLocation {
case .peer, .replyThread, .feed:
case .peer, .replyThread, .customChatContents:
if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags {
if case .standard(.embedded(false)) = presentationInterfaceState.mode {
placeholderText = strings.Common_Search
@ -114,7 +114,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
self.searchBar.prefixString = nil
let placeholderText: String
switch self.chatLocation {
case .peer, .replyThread, .feed:
case .peer, .replyThread, .customChatContents:
if presentationInterfaceState.historyFilter != nil {
placeholderText = self.strings.Common_Search
} else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags {

View File

@ -1517,7 +1517,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
} else {
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true) = chatHistoryState {
if let chatHistoryState = interfaceState.chatHistoryState, case .loaded(true, _) = chatHistoryState {
displayBotStartButton = true
} else if interfaceState.peerIsBlocked {
displayBotStartButton = true
@ -1801,38 +1801,56 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
let dismissedButtonMessageUpdated = interfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != previousState?.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId
let replyMessageUpdated = interfaceState.interfaceState.replyMessageSubject != previousState?.interfaceState.replyMessageSubject
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id {
var peerUpdated = false
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) {
peerUpdated = true
}
if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id {
self.initializedPlaceholder = true
var placeholder: String
var placeholder: String = ""
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if interfaceState.interfaceState.silentPosting {
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
} else {
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
}
} else {
if sendingTextDisabled {
placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed
} else {
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId {
if replyThreadMessage.isChannelPost {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
}
} else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData {
if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo {
placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string
} else {
placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string
}
if let peer = interfaceState.renderedPeer?.peer {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if interfaceState.interfaceState.silentPosting {
placeholder = interfaceState.strings.Conversation_InputTextSilentBroadcastPlaceholder
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder
}
} else {
if sendingTextDisabled {
placeholder = interfaceState.strings.Chat_PlaceholderTextNotAllowed
} else {
if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
placeholder = interfaceState.strings.Conversation_InputTextAnonymousPlaceholder
} else if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation, !replyThreadMessage.isForumPost, replyThreadMessage.peerId != self.context?.account.peerId {
if replyThreadMessage.isChannelPost {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderComment
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholderReply
}
} else if let channel = peer as? TelegramChannel, channel.isForum, let forumTopicData = interfaceState.forumTopicData {
if let replyMessage = interfaceState.replyMessage, let threadInfo = replyMessage.associatedThreadInfo {
placeholder = interfaceState.strings.Chat_InputPlaceholderReplyInTopic(threadInfo.title).string
} else {
placeholder = interfaceState.strings.Chat_InputPlaceholderMessageInTopic(forumTopicData.title).string
}
} else {
placeholder = interfaceState.strings.Conversation_InputTextPlaceholder
}
}
}
}
if case let .customChatContents(customChatContents) = interfaceState.subject {
//TODO:localize
switch customChatContents.kind {
case .greetingMessageInput:
placeholder = "Add greeting message..."
case .awayMessageInput:
placeholder = "Add away message..."
case .quickReplyMessageInput:
placeholder = "Add quick reply..."
}
}

View File

@ -1899,8 +1899,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return BusinessHoursSetupScreen(context: context)
}
public func makeGreetingMessageSetupScreen(context: AccountContext) -> ViewController {
return GreetingMessageSetupScreen(context: context)
public func makeGreetingMessageSetupScreen(context: AccountContext, isAwayMode: Bool) -> ViewController {
return GreetingMessageSetupScreen(context: context, mode: isAwayMode ? .away : .greeting)
}
public func makeQuickReplySetupScreen(context: AccountContext) -> ViewController {
return QuickReplySetupScreen(context: context)
}
public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController {