diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index f423a580d6..230c49dd13 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1093,6 +1093,8 @@ public protocol ChatCustomContentsProtocol: AnyObject { func enqueueMessages(messages: [EnqueueMessage]) func deleteMessages(ids: [EngineMessage.Id]) func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) + + func quickReplyUpdateShortcut(value: String) } public enum ChatHistoryListDisplayHeaders { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 2a6b2cc4f5..0ac6bf755a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2324,6 +2324,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.interaction.openStories?(id, sourceNode.avatarNode) } }, dismissNotice: { _ in + }, editPeer: { _ in }) chatListInteraction.isSearchMode = true @@ -3689,6 +3690,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 760ec06591..109edbc4db 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -157,6 +157,7 @@ public final class ChatListShimmerNode: ASDisplayNode { }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 4396b5b4f4..c307c7573e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -557,6 +557,7 @@ private enum RevealOptionKey: Int32 { case hidePsa case open case close + case edit } private func canArchivePeer(id: EnginePeer.Id, accountPeerId: EnginePeer.Id) -> Bool { @@ -3123,6 +3124,17 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor) ] peerLeftRevealOptions = [] + } else if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { + peerLeftRevealOptions = [] + if customMessageListData.commandPrefix != nil { + //TODO:localize + peerRevealOptions = [ + ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: "Edit", icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor), + ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: "Delete", icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor) + ] + } else { + peerRevealOptions = [] + } } else if promoInfo == nil { peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: !isAccountPeer ? (currentMutedIconImage != nil) : nil, location: item.chatListLocation, peerId: renderedPeer.peerId, accountPeerId: item.context.account.peerId, canDelete: true, isEditing: item.editing, filterData: item.filterData) if case let .chat(itemPeer) = contentPeer { @@ -4356,6 +4368,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.revealOptionsInteractivelyClosed() self.customAnimationInProgress = false } + case RevealOptionKey.edit.rawValue: + item.interaction.editPeer(item) + close = true default: break } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index b638996c25..7306140ff1 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -108,6 +108,7 @@ public final class ChatListNodeInteraction { let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void let dismissNotice: (ChatListNotice) -> Void + let editPeer: (ChatListItem) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -159,7 +160,8 @@ public final class ChatListNodeInteraction { openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void, - dismissNotice: @escaping (ChatListNotice) -> Void + dismissNotice: @escaping (ChatListNotice) -> Void, + editPeer: @escaping (ChatListItem) -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -199,6 +201,7 @@ public final class ChatListNodeInteraction { self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories self.dismissNotice = dismissNotice + self.editPeer = editPeer } } @@ -1785,6 +1788,7 @@ public final class ChatListNode: ListView { default: break } + }, editPeer: { _ in }) nodeInteraction.isInlineMode = isInlineMode diff --git a/submodules/Display/Source/AlertController.swift b/submodules/Display/Source/AlertController.swift index 00bb5cf123..8589a0baef 100644 --- a/submodules/Display/Source/AlertController.swift +++ b/submodules/Display/Source/AlertController.swift @@ -83,7 +83,7 @@ open class AlertController: ViewController, StandalonePresentableController, Key } } } - private let contentNode: AlertContentNode + public let contentNode: AlertContentNode private let allowInputInset: Bool private weak var existingAlertController: AlertController? diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index d0ac26e952..16af2a2c07 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -409,6 +409,8 @@ public final class TextNodeLayout: NSObject { switch self.resolvedAlignment { case .center: lineFrame = CGRect(origin: CGPoint(x: floor((size.width - line.frame.size.width) / 2.0), y: line.frame.minY), size: line.frame.size) + case .right: + lineFrame = CGRect(origin: CGPoint(x: size.width - line.frame.size.width, y: line.frame.minY), size: line.frame.size) default: lineFrame = displayLineFrame(frame: line.frame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: size), cutout: cutout) } @@ -521,6 +523,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -589,6 +593,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -666,6 +672,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -846,6 +854,8 @@ public final class TextNodeLayout: NSObject { lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0) case .natural: lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout) + case .right: + lineFrame.origin.x = self.size.width - lineFrame.size.width default: break } @@ -2407,6 +2417,8 @@ open class TextNode: ASDisplayNode { } else { lineFrame.origin.x += offset.x } + } else if alignment == .right { + lineFrame.origin.x = offset.x + (bounds.size.width - lineFrame.width) } //context.setStrokeColor(UIColor.red.cgColor) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index a9a40ac489..f2ea14db13 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -102,6 +102,7 @@ public final class HashtagSearchController: TelegramBaseController { }, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift index fe08667473..537134acd2 100644 --- a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift +++ b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift @@ -202,11 +202,11 @@ public class ItemListAddressItemNode: ListViewItemNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftOffset - leftInset - rightInset - 98.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let padding: CGFloat = !item.label.isEmpty ? 39.0 : 20.0 - let imageSide = min(90.0, max(46.0, textLayout.size.height + padding - 18.0)) + let imageSide = min(90.0, max(66.0, textLayout.size.height + padding - 18.0)) let imageSize = CGSize(width: imageSide, height: imageSide) let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) - let contentSize = CGSize(width: params.width, height: max(textLayout.size.height + padding, imageSize.height + 18.0)) + let contentSize = CGSize(width: params.width, height: max(textLayout.size.height + padding, imageSize.height + 14.0)) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (nodeLayout, { [weak self] animation in @@ -268,7 +268,7 @@ public class ItemListAddressItemNode: ListViewItemNode { strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 11.0), size: labelLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: item.label.isEmpty ? 11.0 : 31.0), size: textLayout.size) - let imageFrame = CGRect(origin: CGPoint(x: params.width - imageSize.width - rightInset, y: floorToScreenPixels((contentSize.height - imageSize.height) / 2.0)), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: params.width - imageSize.width - rightInset, y: 7.0), size: imageSize) strongSelf.imageNode.frame = imageFrame if let icon = strongSelf.iconNode.image { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index ee2e5da5f9..be55c4f3dc 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1981,15 +1981,38 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { action: { switch perk { case .location: - push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext)) + let _ = (accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in })) + }) case .hours: - push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext)) + let _ = (accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in })) + }) case .quickReplies: - push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext)) + let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData)) + }) case .greetings: - push(accountContext.sharedContext.makeGreetingMessageSetupScreen(context: accountContext, isAwayMode: false)) + push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: false)) case .awayMessages: - push(accountContext.sharedContext.makeGreetingMessageSetupScreen(context: accountContext, isAwayMode: true)) + push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: true)) case .chatbots: push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext)) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 17af5284bf..9bf1d44c49 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -227,6 +227,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index d096a99ee1..f9201ecc1c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -376,6 +376,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) func makeChatListItem( diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index cb3a0680f6..b8a93acb65 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -432,11 +432,11 @@ swift_library( "//submodules/TelegramUI/Components/Chat/TopMessageReactions", "//submodules/TelegramUI/Components/Chat/SavedTagNameAlertController", "//submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent", - "//submodules/TelegramUI/Components/Settings/BusinessSetupScreen", "//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen", "//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen", "//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen", "//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen", + "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 0340332567..4a8a9de071 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -617,6 +617,8 @@ public final class ChatInlineSearchResultsListComponent: Component { openStories: { _, _ in }, dismissNotice: { _ in + }, + editPeer: { _ in } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift index 3eb1ce98c6..98a899b591 100644 --- a/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift +++ b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift @@ -10,6 +10,7 @@ public enum ChatNavigationButtonAction: Equatable { case dismiss case toggleInfoPanel case spacer + case edit } public struct ChatNavigationButton: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift index 0b1f06d683..de95db7203 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift @@ -90,7 +90,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { itemNode = current addressItem.updateNode(async: { $0() }, node: { return itemNode - }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + }, params: params, previousItem: topItem != nil ? addressItem : nil, nextItem: bottomItem != nil ? addressItem : nil, animation: .None, completion: { (layout, apply) in let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height)) itemNode.contentSize = layout.contentSize @@ -119,7 +119,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) - self.bottomSeparatorNode.isHidden = hasBottomCorners + self.bottomSeparatorNode.isHidden = hasBottomCorners || bottomItem != nil transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift new file mode 100644 index 0000000000..2c10f091cc --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -0,0 +1,438 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext +import TextFormat +import UIKit +import AppBundle +import TelegramStringFormatting +import ContextUI +import TelegramCore +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent + +private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> String { + var businessHoursText: String = "" + switch day { + case .open: + businessHoursText += "open 24 hours" + case .closed: + businessHoursText += "closed" + case let .intervals(intervals): + func clipMinutes(_ value: Int) -> Int { + return value % (24 * 60) + } + + var resultText: String = "" + for range in intervals { + if !resultText.isEmpty { + resultText.append("\n") + } + let startHours = clipMinutes(range.startMinute) / 60 + let startMinutes = clipMinutes(range.startMinute) % 60 + let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat()) + let endHours = clipMinutes(range.endMinute) / 60 + let endMinutes = clipMinutes(range.endMinute) % 60 + let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat()) + resultText.append("\(startText) - \(endText)") + } + businessHoursText += resultText + } + + return businessHoursText +} + +final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem { + let id: AnyHashable + let label: String + let businessHours: TelegramBusinessHours + let requestLayout: () -> Void + + init( + id: AnyHashable, + label: String, + businessHours: TelegramBusinessHours, + requestLayout: @escaping () -> Void + ) { + self.id = id + self.label = label + self.businessHours = businessHours + self.requestLayout = requestLayout + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenBusinessHoursItemNode() + } +} + +private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode { + private let containerNode: ContextControllerSourceNode + private let contextSourceNode: ContextExtractedContentContainingNode + + private let extractedBackgroundImageNode: ASImageNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let maskNode: ASImageNode + private let labelNode: ImmediateTextNode + private let currentStatusText = ComponentView() + private let currentDayText = ComponentView() + private var dayTitles: [ComponentView] = [] + private var dayValues: [ComponentView] = [] + private let arrowIcon = ComponentView() + + private let bottomSeparatorNode: ASDisplayNode + + private let activateArea: AccessibilityAreaNode + + private var item: PeerInfoScreenBusinessHoursItem? + private var theme: PresentationTheme? + + private var cachedDays: [TelegramBusinessHours.WeekDay] = [] + + private var isExpanded: Bool = false + + override init() { + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init() + + self.addSubnode(self.bottomSeparatorNode) + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.addSubnode(self.maskNode) + + self.contextSourceNode.contentNode.clipsToBounds = true + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.labelNode) + + self.addSubnode(self.activateArea) + + self.containerNode.isGestureEnabled = false + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let theme = strongSelf.theme else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.list.plainBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { point in + return .keepWithSingleTap + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap, .longTap: + self.isExpanded = !self.isExpanded + self.item?.requestLayout() + default: + break + } + } + default: + break + } + } + + override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenBusinessHoursItem else { + return 10.0 + } + + let businessDays: [TelegramBusinessHours.WeekDay] + if self.item?.businessHours != item.businessHours { + businessDays = item.businessHours.splitIntoWeekDays() + self.cachedDays = businessDays + } else { + businessDays = self.cachedDays + } + + self.item = item + self.theme = presentationData.theme + + let sideInset: CGFloat = 16.0 + safeInsets.left + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + + let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + var topOffset = 10.0 + let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: labelSize) + if labelSize.height > 0.0 { + topOffset += labelSize.height + topOffset += 3.0 + } + + let arrowIconSize = self.arrowIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Item List/DownArrow", + tintColor: presentationData.theme.list.disclosureArrowColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let arrowIconFrame = CGRect(origin: CGPoint(x: width - sideInset + 1.0 - arrowIconSize.width, y: topOffset + 5.0), size: arrowIconSize) + if let arrowIconView = self.arrowIcon.view { + if arrowIconView.superview == nil { + self.contextSourceNode.contentNode.view.addSubview(arrowIconView) + arrowIconView.frame = arrowIconFrame + } + transition.updatePosition(layer: arrowIconView.layer, position: arrowIconFrame.center) + transition.updateBounds(layer: arrowIconView.layer, bounds: CGRect(origin: CGPoint(), size: arrowIconFrame.size)) + transition.updateTransformRotation(view: arrowIconView, angle: self.isExpanded ? CGFloat.pi * 1.0 : CGFloat.pi * 0.0) + } + + let currentStatusTextSize = self.currentStatusText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "Open", font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextSuccessColor)) + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0) + ) + let currentStatusTextFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset), size: currentStatusTextSize) + if let currentStatusTextView = self.currentStatusText.view { + if currentStatusTextView.superview == nil { + currentStatusTextView.layer.anchorPoint = CGPoint() + self.contextSourceNode.contentNode.view.addSubview(currentStatusTextView) + } + transition.updatePosition(layer: currentStatusTextView.layer, position: currentStatusTextFrame.origin) + currentStatusTextView.bounds = CGRect(origin: CGPoint(), size: currentStatusTextFrame.size) + } + + let dayRightInset = sideInset + 17.0 + + var currentCalendar = Calendar(identifier: .gregorian) + currentCalendar.timeZone = TimeZone.current + var currentDayIndex = currentCalendar.component(.weekday, from: Date()) + if currentDayIndex == 1 { + currentDayIndex = 6 + } else { + currentDayIndex -= 2 + } + + var targetCalendar = Calendar(identifier: .gregorian) + targetCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current + //targetCalendar.component(<#T##component: Calendar.Component##Calendar.Component#>, from: <#T##Date#>) + + let currentDayTextSize = self.currentDayText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex]) : " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .right, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0) + ) + let currentDayTextFrame = CGRect(origin: CGPoint(x: width - dayRightInset - currentDayTextSize.width, y: topOffset), size: currentDayTextSize) + if let currentDayTextView = self.currentDayText.view { + if currentDayTextView.superview == nil { + currentDayTextView.layer.anchorPoint = CGPoint() + self.contextSourceNode.contentNode.view.addSubview(currentDayTextView) + } + transition.updatePosition(layer: currentDayTextView.layer, position: currentDayTextFrame.origin) + currentDayTextView.bounds = CGRect(origin: CGPoint(), size: currentDayTextFrame.size) + } + + topOffset += max(currentStatusTextSize.height, currentDayTextSize.height) + + let daySpacing: CGFloat = 15.0 + + var dayHeights: CGFloat = 0.0 + + for i in 0 ..< businessDays.count { + dayHeights += daySpacing + + var dayTransition = transition + let dayTitle: ComponentView + if self.dayTitles.count > i { + dayTitle = self.dayTitles[i] + } else { + dayTransition = .immediate + dayTitle = ComponentView() + self.dayTitles.append(dayTitle) + } + + let dayValue: ComponentView + if self.dayValues.count > i { + dayValue = self.dayValues[i] + } else { + dayValue = ComponentView() + self.dayValues.append(dayValue) + } + + let dayTitleValue: String + //TODO:localize + switch i { + case 0: + dayTitleValue = "Monday" + case 1: + dayTitleValue = "Tuesday" + case 2: + dayTitleValue = "Wednesday" + case 3: + dayTitleValue = "Thursday" + case 4: + dayTitleValue = "Friday" + case 5: + dayTitleValue = "Saturday" + case 6: + dayTitleValue = "Sunday" + default: + dayTitleValue = " " + } + + let businessHoursText = dayBusinessHoursText(businessDays[i]) + + let dayTitleSize = dayTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: dayTitleValue, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: width - sideInset * 2.0, height: 100.0) + ) + let dayTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: topOffset + dayHeights), size: dayTitleSize) + if let dayTitleView = dayTitle.view { + if dayTitleView.superview == nil { + dayTitleView.layer.anchorPoint = CGPoint() + self.contextSourceNode.contentNode.view.addSubview(dayTitleView) + dayTitleView.alpha = 0.0 + } + dayTransition.updatePosition(layer: dayTitleView.layer, position: dayTitleFrame.origin) + dayTitleView.bounds = CGRect(origin: CGPoint(), size: dayTitleFrame.size) + + transition.updateAlpha(layer: dayTitleView.layer, alpha: self.isExpanded ? 1.0 : 0.0) + } + + let dayValueSize = dayValue.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: businessHoursText, font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor, paragraphAlignment: .right)), + horizontalAlignment: .right, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0) + ) + let dayValueFrame = CGRect(origin: CGPoint(x: width - dayRightInset - dayValueSize.width, y: topOffset + dayHeights), size: dayValueSize) + if let dayValueView = dayValue.view { + if dayValueView.superview == nil { + dayValueView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.contextSourceNode.contentNode.view.addSubview(dayValueView) + dayValueView.alpha = 0.0 + } + dayTransition.updatePosition(layer: dayValueView.layer, position: CGPoint(x: dayValueFrame.maxX, y: dayValueFrame.minY)) + dayValueView.bounds = CGRect(origin: CGPoint(), size: dayValueFrame.size) + + transition.updateAlpha(layer: dayValueView.layer, alpha: self.isExpanded ? 1.0 : 0.0) + } + + dayHeights += max(dayTitleSize.height, dayValueSize.height) + } + + if self.isExpanded { + topOffset += dayHeights + } + + topOffset += 11.0 + + transition.updateFrame(node: self.labelNode, frame: labelFrame) + + let height = topOffset + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + let hasCorners = hasCorners && (topItem == nil || bottomItem == nil) + let hasTopCorners = hasCorners && topItem == nil + let hasBottomCorners = hasCorners && bottomItem == nil + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) + self.bottomSeparatorNode.isHidden = hasBottomCorners + + self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)) + self.activateArea.accessibilityLabel = item.label + + let contentSize = CGSize(width: width, height: height) + self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize) + transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize)) + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height)) + let extractedRect = nonExtractedRect + self.extractedRect = extractedRect + self.nonExtractedRect = nonExtractedRect + + if self.contextSourceNode.isExtractedToContextPreview { + self.extractedBackgroundImageNode.frame = extractedRect + } else { + self.extractedBackgroundImageNode.frame = nonExtractedRect + } + self.contextSourceNode.contentRect = extractedRect + + return height + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b25d2960a5..f603ed8b39 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1167,72 +1167,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let businessHours = cachedData.businessHours { //TODO:localize - var businessHoursText: String = "" - let days = businessHours.splitIntoWeekDays() - for i in 0 ..< days.count { - let title: String - //TODO:localize - switch i { - case 0: - title = "Monday" - case 1: - title = "Tuesday" - case 2: - title = "Wednesday" - case 3: - title = "Thursday" - case 4: - title = "Friday" - case 5: - title = "Saturday" - case 6: - title = "Sunday" - default: - title = " " - } - - //TODO:localize - if !businessHoursText.isEmpty { - businessHoursText += "\n" - } - businessHoursText += "\(title): " - switch days[i] { - case .open: - businessHoursText += "open 24 hours" - case .closed: - businessHoursText += "closed" - case let .intervals(intervals): - func clipMinutes(_ value: Int) -> Int { - return value % (24 * 60) - } - - var resultText: String = "" - for range in intervals { - if !resultText.isEmpty { - resultText.append(", ") - } - let startHours = clipMinutes(range.startMinute) / 60 - let startMinutes = clipMinutes(range.startMinute) % 60 - let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat()) - let endHours = clipMinutes(range.endMinute) / 60 - let endMinutes = clipMinutes(range.endMinute) % 60 - let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat()) - resultText.append("\(startText)\u{00a0}- \(endText)") - } - businessHoursText += resultText - } - } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 300, label: "business hours", text: businessHoursText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { - interaction.requestLayout(false) + items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: { + interaction.requestLayout(true) })) } if let businessLocation = cachedData.businessLocation { + //TODO:localize if let coordinates = businessLocation.coordinates { let imageSignal = chatMapSnapshotImage(engine: context.engine, resource: MapSnapshotMediaResource(latitude: coordinates.latitude, longitude: coordinates.longitude, width: 90, height: 90)) items[.peerInfo]!.append(PeerInfoScreenAddressItem( id: 301, - label: "", + label: "location", text: businessLocation.address, imageSignal: imageSignal, action: { @@ -1242,7 +1188,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { items[.peerInfo]!.append(PeerInfoScreenAddressItem( id: 301, - label: "", + label: "location", text: businessLocation.address, imageSignal: nil, action: nil diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index c1f380e630..123a1d0087 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -199,6 +199,8 @@ final class GreetingMessageListItemComponent: Component { openStories: { _, _ in }, dismissNotice: { _ in + }, + editPeer: { _ in } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift index 6b233f77bc..b238231a04 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -168,7 +168,7 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco } } - let kind: ChatCustomContentsKind + var kind: ChatCustomContentsKind var messages: Signal<[Message], NoError> { return self.impl.signalWith({ impl, subscriber in @@ -210,4 +210,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) } } + + func quickReplyUpdateShortcut(value: String) { + self.kind = .quickReplyMessageInput(shortcut: value) + } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index e3eb272ea3..58cf348038 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -139,7 +139,11 @@ final class QuickReplySetupScreenComponent: Component { }, setPeerThreadMuted: { _, _, _ in }, - deletePeer: { _, _ in + deletePeer: { [weak listNode] _, _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.openDeleteShortcut(shortcut: item.shortcut) }, deletePeerThread: { _, _ in }, @@ -184,6 +188,12 @@ final class QuickReplySetupScreenComponent: Component { openStories: { _, _ in }, dismissNotice: { _ in + }, + editPeer: { [weak listNode] _ in + guard let listNode, let parentView = listNode.parentView else { + return + } + parentView.openEditShortcut(shortcut: item.shortcut) } ) @@ -237,7 +247,7 @@ final class QuickReplySetupScreenComponent: Component { hasActiveRevealControls: false, selected: false, header: nil, - enableContextActions: false, + enableContextActions: true, hiddenOffset: false, interaction: chatListNodeInteraction ) @@ -313,6 +323,7 @@ final class QuickReplySetupScreenComponent: Component { private var environment: EnvironmentType? private var items: [QuickReplyMessageShortcut] = [] + private var itemsDisposable: Disposable? private var messagesDisposable: Disposable? private var isEditing: Bool = false @@ -329,6 +340,7 @@ final class QuickReplySetupScreenComponent: Component { } deinit { + self.itemsDisposable?.dispose() self.messagesDisposable?.dispose() } @@ -379,7 +391,7 @@ final class QuickReplySetupScreenComponent: Component { self.messagesDisposable?.dispose() self.messagesDisposable = (contents.messages |> deliverOnMainQueue).startStrict(next: { [weak self] messages in - guard let self else { + guard let self, let component = self.component else { return } let messages = messages.reversed().map(EngineMessage.init) @@ -396,6 +408,8 @@ final class QuickReplySetupScreenComponent: Component { } } + component.context.engine.accountData.updateShortcutMessages(state: QuickReplyMessageShortcutsState(shortcuts: self.items)) + self.state?.updated(transition: .immediate) }) } else { @@ -416,6 +430,13 @@ final class QuickReplySetupScreenComponent: Component { return } if let value, !value.isEmpty { + if self.items.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { + contentNode.setErrorText(errorText: "Shortcut with that name already exists") + } + return + } + alertController?.dismissAnimated() self.openQuickReplyChat(shortcut: value) } @@ -426,6 +447,81 @@ final class QuickReplySetupScreenComponent: Component { self.contentListNode?.clearHighlightAnimated(true) } + func openEditShortcut(shortcut: String) { + guard let component = self.component else { + return + } + + let currentValue = shortcut + + var completion: ((String?) -> Void)? + let alertController = quickReplyNameAlertController( + context: component.context, + text: "Edit Shortcut", + subtext: "Add a new name for your shortcut.", + value: currentValue, + characterLimit: 32, + apply: { value in + completion?(value) + } + ) + completion = { [weak self, weak alertController] value in + guard let self, let component = self.component else { + alertController?.dismissAnimated() + return + } + if let value, !value.isEmpty { + if value == currentValue { + alertController?.dismissAnimated() + return + } + + var shortcuts = self.items + guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == currentValue }) else { + alertController?.dismissAnimated() + return + } + + if shortcuts.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { + contentNode.setErrorText(errorText: "Shortcut with that name already exists") + } + } else { + shortcuts[index] = QuickReplyMessageShortcut( + id: shortcuts[index].id, + shortcut: value, + messages: shortcuts[index].messages + ) + self.items = shortcuts + let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts) + component.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages) + + alertController?.dismissAnimated() + } + } + } + self.environment?.controller()?.present(alertController, in: .window(.root)) + } + + func openDeleteShortcut(shortcut: String) { + guard let component = self.component else { + return + } + + var shortcuts = self.items + guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == shortcut }) else { + return + } + + shortcuts.remove(at: index) + self.items = shortcuts + + self.state?.updated(transition: .spring(duration: 0.4)) + + let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts) + component.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages) + } + private func updateNavigationBar( component: QuickReplySetupScreenComponent, theme: PresentationTheme, @@ -472,7 +568,10 @@ final class QuickReplySetupScreenComponent: Component { guard let self else { return } - self.environment?.controller()?.dismiss() + + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } } ) @@ -576,6 +675,17 @@ final class QuickReplySetupScreenComponent: Component { if self.component == nil { self.accountPeer = component.initialData.accountPeer self.items = component.initialData.shortcutMessages.shortcuts + + self.itemsDisposable = (component.context.engine.accountData.shortcutMessages() + |> deliverOnMainQueue).start(next: { [weak self] shortcutMessages in + guard let self else { + return + } + self.items = shortcutMessages.shortcuts + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) } let environment = environment[EnvironmentType.self].value diff --git a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD deleted file mode 100644 index 6866f506c4..0000000000 --- a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/BUILD +++ /dev/null @@ -1,35 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "BusinessSetupScreen", - module_name = "BusinessSetupScreen", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/AsyncDisplayKit", - "//submodules/Display", - "//submodules/Postbox", - "//submodules/TelegramCore", - "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/TelegramPresentationData", - "//submodules/AccountContext", - "//submodules/PresentationDataUtils", - "//submodules/ComponentFlow", - "//submodules/Components/ViewControllerComponent", - "//submodules/Components/BundleIconComponent", - "//submodules/TelegramUI/Components/AnimatedTextComponent", - "//submodules/Components/MultilineTextComponent", - "//submodules/Components/BalancedTextComponent", - "//submodules/TelegramUI/Components/ButtonComponent", - "//submodules/TelegramUI/Components/BackButtonComponent", - "//submodules/TelegramUI/Components/ListSectionComponent", - "//submodules/TelegramUI/Components/ListActionItemComponent", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift deleted file mode 100644 index 321e821184..0000000000 --- a/submodules/TelegramUI/Components/Settings/BusinessSetupScreen/Sources/BusinessSetupScreen.swift +++ /dev/null @@ -1,475 +0,0 @@ -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 MultilineTextComponent -import BalancedTextComponent -import BackButtonComponent -import ListSectionComponent -import ListActionItemComponent -import BundleIconComponent - -final class BusinessSetupScreenComponent: Component { - typealias EnvironmentType = ViewControllerComponentContainer.Environment - - let context: AccountContext - - init( - context: AccountContext - ) { - self.context = context - } - - static func ==(lhs: BusinessSetupScreenComponent, rhs: BusinessSetupScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - - return true - } - - private final class ScrollView: UIScrollView { - override func touchesShouldCancel(in view: UIView) -> Bool { - return true - } - } - - final class View: UIView, UIScrollViewDelegate { - private let topOverscrollLayer = SimpleLayer() - private let scrollView: ScrollView - - private let navigationTitle = ComponentView() - private let title = ComponentView() - private let subtitle = ComponentView() - private let actionsSection = ComponentView() - - private var isUpdating: Bool = false - - private var component: BusinessSetupScreenComponent? - private(set) weak var state: EmptyComponentState? - private var environment: EnvironmentType? - - private var businessHours: TelegramBusinessHours? - private var businessLocation: TelegramBusinessLocation? - private var dataDisposable: Disposable? - - override init(frame: CGRect) { - self.scrollView = ScrollView() - self.scrollView.showsVerticalScrollIndicator = true - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.scrollsToTop = false - self.scrollView.delaysContentTouches = false - self.scrollView.canCancelContentTouches = true - self.scrollView.contentInsetAdjustmentBehavior = .never - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.alwaysBounceVertical = true - - super.init(frame: frame) - - self.scrollView.delegate = self - self.addSubview(self.scrollView) - - self.scrollView.layer.addSublayer(self.topOverscrollLayer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.dataDisposable?.dispose() - } - - func scrollToTop() { - self.scrollView.setContentOffset(CGPoint(), animated: true) - } - - func attemptNavigation(complete: @escaping () -> Void) -> Bool { - return true - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrolling(transition: .immediate) - } - - var scrolledUp = true - private func updateScrolling(transition: Transition) { - guard let environment = self.environment else { - return - } - - let navigationRevealOffsetY: CGFloat = -(environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) * 0.5) + (self.title.view?.frame.midY ?? 0.0) - - let navigationAlphaDistance: CGFloat = 16.0 - let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) - transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) - } - - var scrolledUp = false - if navigationAlpha < 0.5 { - scrolledUp = true - } else if navigationAlpha > 0.5 { - scrolledUp = false - } - - if self.scrolledUp != scrolledUp { - self.scrolledUp = scrolledUp - if !self.isUpdating { - self.state?.updated() - } - } - - if let navigationTitleView = self.navigationTitle.view { - transition.setAlpha(view: navigationTitleView, alpha: navigationAlpha) - } - } - - func update(component: BusinessSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - if self.dataDisposable == nil { - self.dataDisposable = (component.context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.BusinessHours(id: component.context.account.peerId), - TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: component.context.account.peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] businessHours, businessLocation in - guard let self else { - return - } - self.businessHours = businessHours - self.businessLocation = businessLocation - }) - } - - let environment = environment[EnvironmentType.self].value - let themeUpdated = self.environment?.theme !== environment.theme - self.environment = environment - - self.component = component - self.state = state - - if themeUpdated { - self.backgroundColor = environment.theme.list.blocksBackgroundColor - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - //TODO:localize - let navigationTitleSize = self.navigationTitle.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Telegram Business", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) - ) - let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) - if let navigationTitleView = self.navigationTitle.view { - if navigationTitleView.superview == nil { - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - navigationBar.view.addSubview(navigationTitleView) - } - } - transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) - } - - let bottomContentInset: CGFloat = 24.0 - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let sectionSpacing: CGFloat = 32.0 - - let _ = bottomContentInset - let _ = sectionSpacing - - var contentHeight: CGFloat = 0.0 - - contentHeight += environment.navigationHeight - contentHeight += 16.0 - - //TODO:localize - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Telegram Business", font: Font.bold(29.0), textColor: environment.theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.scrollView.addSubview(titleView) - } - transition.setPosition(view: titleView, position: titleFrame.center) - titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - } - contentHeight += titleSize.height - contentHeight += 17.0 - - //TODO:localize - let subtitleSize = self.subtitle.update( - transition: .immediate, - component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "You have now unlocked these additional business features.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.25 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) - if let subtitleView = self.subtitle.view { - if subtitleView.superview == nil { - self.scrollView.addSubview(subtitleView) - } - transition.setPosition(view: subtitleView, position: subtitleFrame.center) - subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) - } - contentHeight += subtitleSize.height - contentHeight += 21.0 - - struct Item { - var icon: String - var title: String - var subtitle: String - var action: () -> Void - } - var items: [Item] = [] - //TODO:localize - items.append(Item( - icon: "Settings/Menu/AddAccount", - title: "Location", - subtitle: "Display the location of your business on your account.", - action: { [weak self] in - guard let self else { - return - } - self.environment?.controller()?.push(component.context.sharedContext.makeBusinessLocationSetupScreen(context: component.context, initialValue: self.businessLocation, completion: { _ in - })) - } - )) - items.append(Item( - icon: "Settings/Menu/DataVoice", - title: "Opening Hours", - subtitle: "Show to your customers when you are open for business.", - action: { [weak self] in - guard let self else { - return - } - self.environment?.controller()?.push(component.context.sharedContext.makeBusinessHoursSetupScreen(context: component.context, initialValue: self.businessHours, completion: { _ in })) - } - )) - items.append(Item( - icon: "Settings/Menu/Photos", - title: "Quick Replies", - subtitle: "Set up shortcuts with rich text and media to respond to messages faster.", - action: { [weak self] in - guard let self, let component = self.component else { - return - } - - let _ = (component.context.sharedContext.makeQuickReplySetupScreenInitialData(context: component.context) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] initialData in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - environment.controller()?.push(component.context.sharedContext.makeQuickReplySetupScreen(context: component.context, initialData: initialData)) - }) - } - )) - items.append(Item( - icon: "Settings/Menu/Stories", - title: "Greeting Messages", - subtitle: "Create greetings that will be automatically sent to new customers.", - action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - environment.controller()?.push(component.context.sharedContext.makeAutomaticBusinessMessageSetupScreen(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: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - environment.controller()?.push(component.context.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: component.context, isAwayMode: true)) - } - )) - items.append(Item( - icon: "Settings/Menu/DataStickers", - title: "Chatbots", - subtitle: "Add any third-party chatbots that will process customer interactions.", - action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment else { - return - } - environment.controller()?.push(component.context.sharedContext.makeChatbotSetupScreen(context: component.context)) - } - )) - - var actionsSectionItems: [AnyComponentWithIdentity] = [] - for item in items { - actionsSectionItems.append(AnyComponentWithIdentity(id: actionsSectionItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: item.title, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 0 - ))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: item.subtitle, - font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)), - textColor: environment.theme.list.itemSecondaryTextColor - )), - maximumNumberOfLines: 0, - lineSpacing: 0.18 - ))) - ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( - name: item.icon, - tintColor: nil - ))), - action: { _ in - item.action() - } - )))) - } - - let actionsSectionSize = self.actionsSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: nil, - footer: nil, - items: actionsSectionItems - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let actionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: actionsSectionSize) - if let actionsSectionView = self.actionsSection.view { - if actionsSectionView.superview == nil { - self.scrollView.addSubview(actionsSectionView) - } - transition.setFrame(view: actionsSectionView, frame: actionsSectionFrame) - } - contentHeight += actionsSectionSize.height - - contentHeight += bottomContentInset - contentHeight += environment.safeInsets.bottom - - let previousBounds = self.scrollView.bounds - - let contentSize = CGSize(width: availableSize.width, height: contentHeight) - if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { - self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) - } - if self.scrollView.contentSize != contentSize { - self.scrollView.contentSize = contentSize - } - let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) - if self.scrollView.scrollIndicatorInsets != scrollInsets { - self.scrollView.scrollIndicatorInsets = scrollInsets - } - - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - - self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) - - self.updateScrolling(transition: transition) - - return availableSize - } - } - - func makeView() -> View { - return View() - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -public final class BusinessSetupScreen: ViewControllerComponentContainer { - private let context: AccountContext - - public init(context: AccountContext) { - self.context = context - - super.init(context: context, component: BusinessSetupScreenComponent( - context: context - ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.title = "" - 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? BusinessSetupScreenComponent.View else { - return - } - componentView.scrollToTop() - } - - self.attemptNavigation = { [weak self] complete in - guard let self, let componentView = self.node.hostView.componentView as? BusinessSetupScreenComponent.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) - } -} diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift index f5471769eb..a07d36bff0 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -187,7 +187,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg } } -private final class QuickReplyNameAlertContentNode: AlertContentNode { +public final class QuickReplyNameAlertContentNode: AlertContentNode { private let context: AccountContext private var theme: AlertControllerTheme private let strings: PresentationStrings @@ -198,7 +198,7 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { private let textView = ComponentView() private let subtextView = ComponentView() - let inputFieldNode: PromptInputFieldNode + fileprivate let inputFieldNode: PromptInputFieldNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] @@ -207,6 +207,7 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { private let disposable = MetaDisposable() private var validLayout: CGSize? + private var errorText: String? private let hapticFeedback = HapticFeedback() @@ -216,7 +217,7 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { } } - override var dismissOnOutsideTap: Bool { + override public var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled } @@ -289,8 +290,20 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { var value: String { return self.inputFieldNode.text } + + public func setErrorText(errorText: String?) { + if self.errorText != errorText { + self.errorText = errorText + self.requestLayout?(.immediate) + } + + if errorText != nil { + HapticFeedback().error() + self.inputFieldNode.layer.addShakeAnimation() + } + } - override func updateTheme(_ theme: AlertControllerTheme) { + override public func updateTheme(_ theme: AlertControllerTheme) { self.theme = theme self.actionNodesSeparator.backgroundColor = theme.separatorColor @@ -306,7 +319,7 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { } } - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + override public 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) @@ -332,16 +345,18 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { 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 { + textComponentView.layer.anchorPoint = CGPoint() self.view.addSubview(textComponentView) } - textComponentView.frame = textFrame + textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) } 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)), + text: .plain(NSAttributedString(string: self.errorText ?? self.subtext, font: Font.regular(13.0), textColor: self.errorText != nil ? self.theme.destructiveColor : self.theme.primaryColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), @@ -351,9 +366,11 @@ private final class QuickReplyNameAlertContentNode: AlertContentNode { 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 { + subtextComponentView.layer.anchorPoint = CGPoint() self.view.addSubview(subtextComponentView) } - subtextComponentView.frame = subtextFrame + subtextComponentView.bounds = CGRect(origin: CGPoint(), size: subtextFrame.size) + transition.updatePosition(layer: subtextComponentView.layer, position: subtextFrame.origin) } origin.y += subtextSize.height + 6.0 + spacing diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 4944cbc942..c18ef7ae63 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -870,6 +870,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json new file mode 100644 index 0000000000..ea04ba3497 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf new file mode 100644 index 0000000000..2437bf015c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf @@ -0,0 +1,161 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 12.000000 8.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.000000 -1.000000 1.000000 0.000000 3.804688 6.500000 cm +0.000000 0.000000 0.000000 scn +0.707107 7.902419 m +0.316583 8.292944 -0.316583 8.292944 -0.707107 7.902419 c +-1.097631 7.511895 -1.097631 6.878730 -0.707107 6.488206 c +0.707107 7.902419 l +h +5.000000 2.195312 m +5.707107 1.488206 l +6.097631 1.878730 6.097631 2.511895 5.707107 2.902419 c +5.000000 2.195312 l +h +-0.707107 -2.097581 m +-1.097631 -2.488105 -1.097631 -3.121270 -0.707107 -3.511794 c +-0.316583 -3.902319 0.316583 -3.902319 0.707107 -3.511794 c +-0.707107 -2.097581 l +h +-0.707107 6.488206 m +4.292893 1.488206 l +5.707107 2.902419 l +0.707107 7.902419 l +-0.707107 6.488206 l +h +4.292893 2.902419 m +-0.707107 -2.097581 l +0.707107 -3.511794 l +5.707107 1.488206 l +4.292893 2.902419 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 779 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 12.000000 8.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 8.000000 m +12.000000 8.000000 l +12.000000 0.000000 l +0.000000 0.000000 l +0.000000 8.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 229 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 12.000000 8.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001036 00000 n +0000001058 00000 n +0000001534 00000 n +0000001556 00000 n +0000001854 00000 n +0000001956 00000 n +0000001977 00000 n +0000002149 00000 n +0000002223 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2283 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a769e8a21f..d26e815e00 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -719,6 +719,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject { + switch customChatContents.kind { + case .greetingMessageInput, .awayMessageInput: + break + case .quickReplyMessageInput: + if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty { + //TODO:localize + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: "Remove Shortcut", text: "You didn't create a quick reply message. Exiting will remove the shortcut.", actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: "Remove", action: { [weak strongSelf] in + strongSelf?.dismiss() + }) + ]), in: .window(.root)) + + return false + } + } + } + return true } @@ -12811,7 +12830,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .search: self.interfaceInteraction?.beginMessageSearch(.everything, "") case .dismiss: - self.dismiss() + if self.attemptNavigation({}) { + self.dismiss() + } case .clearCache: let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) self.present(controller, in: .window(.root)) @@ -13019,6 +13040,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .customChatContents: break } + case .edit: + self.editChat() } } diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift new file mode 100644 index 0000000000..59f7290162 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -0,0 +1,78 @@ +import Foundation +import TelegramPresentationData +import AccountContext +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import TelegramPresentationData +import PresentationDataUtils +import QuickReplyNameAlertController + +extension ChatControllerImpl { + func editChat() { + if case let .customChatContents(customChatContents) = self.subject, case let .quickReplyMessageInput(currentValue) = customChatContents.kind { + var completion: ((String?) -> Void)? + let alertController = quickReplyNameAlertController( + context: self.context, + text: "Edit Shortcut", + subtext: "Add a new name for your shortcut.", + value: currentValue, + 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 { + if value == currentValue { + alertController?.dismissAnimated() + return + } + + let _ = (self.context.engine.accountData.shortcutMessages() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] shortcutMessages in + guard let self else { + alertController?.dismissAnimated() + return + } + + var shortcuts = shortcutMessages.shortcuts + guard let index = shortcuts.firstIndex(where: { $0.shortcut.lowercased() == currentValue }) else { + alertController?.dismissAnimated() + return + } + + if shortcuts.contains(where: { $0.shortcut.lowercased() == value.lowercased() }) { + if let contentNode = alertController?.contentNode as? QuickReplyNameAlertContentNode { + contentNode.setErrorText(errorText: "Shortcut with that name already exists") + } + } else { + shortcuts[index] = QuickReplyMessageShortcut( + id: shortcuts[index].id, + shortcut: value, + messages: shortcuts[index].messages + ) + let updatedShortcutMessages = QuickReplyMessageShortcutsState(shortcuts: shortcuts) + self.context.engine.accountData.updateShortcutMessages(state: updatedShortcutMessages) + + self.chatTitleView?.titleContent = .custom("/\(value)", nil, false) + + if case let .customChatContents(customChatContents) = self.subject { + customChatContents.quickReplyUpdateShortcut(value: value) + } + + alertController?.dismissAnimated() + } + }) + } + } + self.present(alertController, in: .window(.root)) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 6098927bc0..013b467cba 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -24,8 +24,8 @@ private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize } -private let titleFont = Font.medium(15.0) -private let messageFont = Font.regular(14.0) +private let titleFont = Font.semibold(15.0) +private let messageFont = Font.regular(13.0) private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNodeContent { private let textNode: ImmediateTextNode @@ -739,8 +739,8 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC 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." + "· 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 { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 3af4fe35df..332d725dd7 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -516,6 +516,64 @@ func chatHistoryEntriesForView( } } + if let subject = associatedData.subject, case let .customChatContents(customChatContents) = subject, case .quickReplyMessageInput = customChatContents.kind { + if !view.isLoading && view.laterId == nil && !view.entries.isEmpty { + for i in 0 ..< 2 { + let string = i == 1 ? "To edit or delete your quick reply, tap an hold on it." : "To use this quick reply in a chat, type / and select the shortcut from the list." + let formattedString = parseMarkdownIntoAttributedString( + string, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black), + bold: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white), + linkAttribute: { url in + return ("URL", url) + } + ) + ) + var entities: [MessageTextEntity] = [] + formattedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in + if let value = value as? UIColor, value == .white { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Bold)) + } + }) + formattedString.enumerateAttribute(NSAttributedString.Key(rawValue: "URL"), in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in + if value != nil { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextMention(peerId: context.account.peerId))) + } + }) + + let message = Message( + stableId: UInt32.max - 1001 - UInt32(i), + stableVersion: 0, + id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: 123 - Int32(i)), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: Int32(i), + flags: [.Incoming], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: nil, + text: "", + attributes: [], + media: [TelegramMediaAction(action: .customText(text: formattedString.string, entities: entities, additionalAttributes: nil))], + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0) + } + } + } + if reverse { return entries.reversed() } else { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 517644b8fe..3b7ddd21f0 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -460,6 +460,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var enableUnreadAlignment: Bool = true private var historyView: ChatHistoryView? + public var originalHistoryView: MessageHistoryView? { + return self.historyView?.originalView + } private let historyDisposable = MetaDisposable() private let readHistoryDisposable = MetaDisposable() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 4d546af84f..0c7316d4a2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -55,11 +55,22 @@ 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)) + if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { + switch customChatContents.kind { + case .greetingMessageInput, .awayMessageInput: + if case .spacer = currentButton?.action { + return currentButton + } else { + return ChatNavigationButton(action: .spacer, buttonItem: UIBarButtonItem(title: " ", style: .plain, target: nil, action: nil)) + } + case .quickReplyMessageInput: + if let currentButton = currentButton, currentButton.action == .dismiss { + return currentButton + } else { + let buttonItem = UIBarButtonItem(title: strings.Common_Close, style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = strings.Common_Close + return ChatNavigationButton(action: .dismiss, buttonItem: buttonItem) + } } } @@ -117,13 +128,24 @@ 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 let .customChatContents(customChatContents) = presentationInterfaceState.subject { + switch customChatContents.kind { + case .greetingMessageInput, .awayMessageInput: + 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) + } + case .quickReplyMessageInput: + if let currentButton = currentButton, currentButton.action == .edit { + return currentButton + } else { + let buttonItem = UIBarButtonItem(title: strings.Common_Edit, style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = strings.Common_Done + return ChatNavigationButton(action: .edit, buttonItem: buttonItem) + } } } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 757c4e17b4..d55b4b900b 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -289,6 +289,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, hideChatFolderUpdates: { }, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index fbb61217d9..e7f2bca16b 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -15,6 +15,7 @@ import ChatContextQuery import ChatInputContextPanelNode import ChatListUI import ComponentFlow +import ComponentDisplayAdapters private enum CommandChatInputContextPanelEntryStableId: Hashable { case editShortcuts @@ -165,6 +166,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { openStories: { _, _ in }, dismissNotice: { _ in + }, + editPeer: { _ in } ) @@ -479,8 +482,12 @@ final class CommandChatInputContextPanelNode: ChatInputContextPanelNode { let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve) + self.contentOffsetChangeTransition = Transition(transition) + self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.contentOffsetChangeTransition = nil + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2c10b8cfff..c16633947b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -51,7 +51,6 @@ import PeerInfoScreen import ChatQrCodeScreen import UndoUI import ChatMessageNotificationItem -import BusinessSetupScreen import ChatbotSetupScreen import BusinessLocationSetupScreen import BusinessHoursSetupScreen