[WIP] Business

This commit is contained in:
Isaac 2024-02-20 22:52:04 +04:00
parent cf5be08c4a
commit 3bc17a648d
36 changed files with 1050 additions and 613 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,6 +102,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)

View File

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

View File

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

View File

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

View File

@ -376,6 +376,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})
func makeChatListItem(

View File

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

View File

@ -617,6 +617,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
openStories: { _, _ in
},
dismissNotice: { _ in
},
editPeer: { _ in
}
)
self.chatListNodeInteraction = chatListNodeInteraction

View File

@ -10,6 +10,7 @@ public enum ChatNavigationButtonAction: Equatable {
case dismiss
case toggleInfoPanel
case spacer
case edit
}
public struct ChatNavigationButton: Equatable {

View File

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

View File

@ -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<Empty>()
private let currentDayText = ComponentView<Empty>()
private var dayTitles: [ComponentView<Empty>] = []
private var dayValues: [ComponentView<Empty>] = []
private let arrowIcon = ComponentView<Empty>()
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<Empty>
if self.dayTitles.count > i {
dayTitle = self.dayTitles[i]
} else {
dayTransition = .immediate
dayTitle = ComponentView()
self.dayTitles.append(dayTitle)
}
let dayValue: ComponentView<Empty>
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?) {
}
}

View File

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

View File

@ -199,6 +199,8 @@ final class GreetingMessageListItemComponent: Component {
openStories: { _, _ in
},
dismissNotice: { _ in
},
editPeer: { _ in
}
)
self.chatListNodeInteraction = chatListNodeInteraction

View File

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

View File

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

View File

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

View File

@ -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<Empty>()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let actionsSection = ComponentView<Empty>()
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<EnvironmentType>, 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<Empty>] = []
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<EnvironmentType>, 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)
}
}

View File

@ -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<Empty>()
private let subtextView = ComponentView<Empty>()
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

View File

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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "arrow (1).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
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 {

View File

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

View File

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

View File

@ -289,6 +289,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})
interaction.searchTextHighightState = searchQuery
self.interaction = interaction

View File

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

View File

@ -51,7 +51,6 @@ import PeerInfoScreen
import ChatQrCodeScreen
import UndoUI
import ChatMessageNotificationItem
import BusinessSetupScreen
import ChatbotSetupScreen
import BusinessLocationSetupScreen
import BusinessHoursSetupScreen