mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Business
This commit is contained in:
parent
cf5be08c4a
commit
3bc17a648d
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
|
@ -102,6 +102,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
|
||||
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -376,6 +376,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
|
||||
func makeChatListItem(
|
||||
|
@ -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": [],
|
||||
|
@ -617,6 +617,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
||||
openStories: { _, _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
},
|
||||
editPeer: { _ in
|
||||
}
|
||||
)
|
||||
self.chatListNodeInteraction = chatListNodeInteraction
|
||||
|
@ -10,6 +10,7 @@ public enum ChatNavigationButtonAction: Equatable {
|
||||
case dismiss
|
||||
case toggleInfoPanel
|
||||
case spacer
|
||||
case edit
|
||||
}
|
||||
|
||||
public struct ChatNavigationButton: Equatable {
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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?) {
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -199,6 +199,8 @@ final class GreetingMessageListItemComponent: Component {
|
||||
openStories: { _, _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
},
|
||||
editPeer: { _ in
|
||||
}
|
||||
)
|
||||
self.chatListNodeInteraction = chatListNodeInteraction
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "arrow (1).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
161
submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf
vendored
Normal file
161
submodules/TelegramUI/Images.xcassets/Item List/DownArrow.imageset/arrow (1).pdf
vendored
Normal 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
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
78
submodules/TelegramUI/Sources/ChatControllerEditChat.swift
Normal file
78
submodules/TelegramUI/Sources/ChatControllerEditChat.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,6 +289,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
interaction.searchTextHighightState = searchQuery
|
||||
self.interaction = interaction
|
||||
|
@ -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()
|
||||
|
@ -51,7 +51,6 @@ import PeerInfoScreen
|
||||
import ChatQrCodeScreen
|
||||
import UndoUI
|
||||
import ChatMessageNotificationItem
|
||||
import BusinessSetupScreen
|
||||
import ChatbotSetupScreen
|
||||
import BusinessLocationSetupScreen
|
||||
import BusinessHoursSetupScreen
|
||||
|
Loading…
x
Reference in New Issue
Block a user