Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,62 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AutomaticBusinessMessageSetupScreen",
module_name = "AutomaticBusinessMessageSetupScreen",
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/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/BackButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
"//submodules/ShimmerEffect",
"//submodules/ChatListUI",
"//submodules/MergeLists",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/ItemListPeerActionItem",
"//submodules/ItemListUI",
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
"//submodules/DateSelectionUI",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/TimeSelectionActionSheet",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/AttachmentUI",
"//submodules/SearchBarNode",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ListItemSwipeOptionContainer",
"//submodules/UndoUI",
"//submodules/ShareController",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,337 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ListSectionComponent
import TelegramPresentationData
import AppBundle
import ChatListUI
import AccountContext
import Postbox
import TelegramCore
final class GreetingMessageListItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let accountPeer: EnginePeer
let message: EngineMessage
let count: Int
let action: (() -> Void)?
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
accountPeer: EnginePeer,
message: EngineMessage,
count: Int,
action: (() -> Void)? = nil
) {
self.context = context
self.theme = theme
self.strings = strings
self.accountPeer = accountPeer
self.message = message
self.count = count
self.action = action
}
static func ==(lhs: GreetingMessageListItemComponent, rhs: GreetingMessageListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.accountPeer != rhs.accountPeer {
return false
}
if lhs.message != rhs.message {
return false
}
if lhs.count != rhs.count {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
final class View: HighlightTrackingButton, ListSectionComponent.ChildView {
private var component: GreetingMessageListItemComponent?
private weak var componentState: EmptyComponentState?
private var chatListPresentationData: ChatListPresentationData?
private var chatListNodeInteraction: ChatListNodeInteraction?
private var itemNode: ListViewItemNode?
var customUpdateIsHighlighted: ((Bool) -> Void)?
var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.internalHighligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, component.action != nil else {
return
}
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
customUpdateIsHighlighted(isHighlighted)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action?()
}
func update(component: GreetingMessageListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.componentState = state
self.isEnabled = component.action != nil
let chatListPresentationData: ChatListPresentationData
if let current = self.chatListPresentationData, let previousComponent, previousComponent.theme === component.theme {
chatListPresentationData = current
} else {
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
chatListPresentationData = ChatListPresentationData(
theme: component.theme,
fontSize: presentationData.listsFontSize,
strings: component.strings,
dateTimeFormat: presentationData.dateTimeFormat,
nameSortOrder: presentationData.nameSortOrder,
nameDisplayOrder: presentationData.nameDisplayOrder,
disableAnimations: false
)
self.chatListPresentationData = chatListPresentationData
}
let chatListNodeInteraction: ChatListNodeInteraction
if let current = self.chatListNodeInteraction {
chatListNodeInteraction = current
} else {
chatListNodeInteraction = ChatListNodeInteraction(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
activateSearch: {
},
peerSelected: { _, _, _, _, _ in
},
disabledPeerSelected: { _, _, _ in
},
togglePeerSelected: { _, _ in
},
togglePeersSelection: { _, _ in
},
additionalCategorySelected: { _ in
},
messageSelected: { _, _, _, _ in
},
groupSelected: { _ in
},
addContact: { _ in
},
setPeerIdWithRevealedOptions: { _, _ in
},
setItemPinned: { _, _ in
},
setPeerMuted: { _, _ in
},
setPeerThreadMuted: { _, _, _ in
},
deletePeer: { _, _ in
},
deletePeerThread: { _, _ in
},
setPeerThreadStopped: { _, _, _ in
},
setPeerThreadPinned: { _, _, _ in
},
setPeerThreadHidden: { _, _, _ in
},
updatePeerGrouping: { _, _ in
},
togglePeerMarkedUnread: { _, _ in
},
toggleArchivedFolderHiddenByDefault: {
},
toggleThreadsSelection: { _, _ in
},
hidePsa: { _ in
},
activateChatPreview: { _, _, _, _, _ in
},
present: { _ in
},
openForumThread: { _, _ in
},
openStorageManagement: {
},
openPasswordSetup: {
},
openPremiumIntro: {
},
openPremiumGift: { _, _ in
},
openPremiumManagement: {
},
openActiveSessions: {
},
openBirthdaySetup: {
},
performActiveSessionAction: { _, _ in
},
openChatFolderUpdates: {
},
hideChatFolderUpdates: {
},
openStories: { _, _ in
},
openStarsTopup: { _ in
},
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
},
openPhotoSetup: {
},
openAdInfo: { _, _ in
},
openAccountFreezeInfo: {
},
openUrl: { _ in
}
)
self.chatListNodeInteraction = chatListNodeInteraction
}
let chatListItem = ChatListItem(
presentationData: chatListPresentationData,
context: component.context,
chatListLocation: .chatList(groupId: .root),
filterData: nil,
index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: component.message.index)),
content: .peer(ChatListItemContent.PeerData(
messages: [component.message],
peer: EngineRenderedPeer(peer: component.accountPeer),
threadInfo: nil,
combinedReadState: nil,
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: nil,
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: [],
customMessageListData: ChatListItemContent.CustomMessageListData(
commandPrefix: nil,
searchQuery: nil,
messageCount: component.count,
hideSeparator: true,
hideDate: true,
hidePeerStatus: true
)
)),
editing: false,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: nil,
hiddenOffset: false,
interaction: chatListNodeInteraction
)
var itemNode: ListViewItemNode?
let params = ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0)
if let current = self.itemNode {
itemNode = current
chatListItem.updateNode(
async: { f in f () },
node: {
return current
},
params: params,
previousItem: nil,
nextItem: nil, animation: .None,
completion: { layout, apply in
let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
current.contentSize = layout.contentSize
current.insets = layout.insets
current.frame = nodeFrame
apply(ListViewItemApply(isOnScreen: true))
})
} else {
var outItemNode: ListViewItemNode?
chatListItem.nodeConfiguredForParams(
async: { f in f() },
params: params,
synchronousLoads: true,
previousItem: nil,
nextItem: nil,
completion: { node, apply in
outItemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
}
)
itemNode = outItemNode
}
let size = CGSize(width: availableSize.width, height: itemNode?.contentSize.height ?? 44.0)
if self.itemNode !== itemNode {
self.itemNode?.removeFromSupernode()
self.itemNode = itemNode
if let itemNode {
itemNode.isUserInteractionEnabled = false
self.addSubview(itemNode.view)
}
}
if let itemNode = self.itemNode {
itemNode.frame = CGRect(origin: CGPoint(), size: size)
}
self.separatorInset = 76.0
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,265 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtocol {
private final class PendingMessageContext {
let disposable = MetaDisposable()
var message: Message?
init() {
}
}
private final class Impl {
let queue: Queue
let context: AccountContext
private var shortcut: String
private var shortcutId: Int32?
private(set) var mergedHistoryView: MessageHistoryView?
private var sourceHistoryView: MessageHistoryView?
private var pendingMessages: [PendingMessageContext] = []
private var historyViewDisposable: Disposable?
private var pendingHistoryViewDisposable: Disposable?
let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>()
private var nextUpdateIsHoleFill: Bool = false
init(queue: Queue, context: AccountContext, shortcut: String, shortcutId: Int32?) {
self.queue = queue
self.context = context
self.shortcut = shortcut
self.shortcutId = shortcutId
self.updateHistoryViewRequest(reload: false)
}
deinit {
for context in self.pendingMessages {
context.disposable.dispose()
}
self.historyViewDisposable?.dispose()
self.pendingHistoryViewDisposable?.dispose()
}
private func updateHistoryViewRequest(reload: Bool) {
if let shortcutId = self.shortcutId {
self.pendingHistoryViewDisposable?.dispose()
self.pendingHistoryViewDisposable = nil
if self.historyViewDisposable == nil || reload {
self.historyViewDisposable?.dispose()
self.historyViewDisposable = (self.context.account.viewTracker.quickReplyMessagesViewForLocation(quickReplyId: shortcutId)
|> deliverOn(self.queue)).start(next: { [weak self] view, update, _ in
guard let self else {
return
}
if update == .FillHole {
self.nextUpdateIsHoleFill = true
self.updateHistoryViewRequest(reload: true)
return
}
let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill
self.nextUpdateIsHoleFill = false
self.sourceHistoryView = view
if !view.entries.contains(where: { $0.message.id.namespace == Namespaces.Message.QuickReplyCloud }) {
self.shortcutId = nil
}
self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic)
})
}
} else {
self.historyViewDisposable?.dispose()
self.historyViewDisposable = nil
self.pendingHistoryViewDisposable = (self.context.account.viewTracker.pendingQuickReplyMessagesViewForLocation(shortcut: self.shortcut)
|> deliverOn(self.queue)).start(next: { [weak self] view, _, _ in
guard let self else {
return
}
let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill
self.nextUpdateIsHoleFill = false
self.sourceHistoryView = view
self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic)
})
}
}
private func updateHistoryView(updateType: ViewUpdateType) {
var entries = self.sourceHistoryView?.entries ?? []
for pendingMessage in self.pendingMessages {
if let message = pendingMessage.message {
if !entries.contains(where: { $0.message.stableId == message.stableId }) {
entries.append(MessageHistoryEntry(
message: message,
isRead: true,
location: nil,
monthLocation: nil,
attributes: MutableMessageHistoryEntryAttributes(
authorIsContact: false
)
))
}
}
}
entries.sort(by: { $0.message.index < $1.message.index })
let mergedHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: entries, holeEarlier: false, holeLater: false, isLoading: false)
self.mergedHistoryView = mergedHistoryView
self.historyViewStream.putNext((mergedHistoryView, updateType))
}
func enqueueMessages(messages: [EnqueueMessage]) {
let threadId = self.shortcutId.flatMap(Int64.init)
let _ = (TelegramCore.enqueueMessages(account: self.context.account, peerId: self.context.account.peerId, messages: messages.map { message in
return message.withUpdatedThreadId(threadId).withUpdatedAttributes { attributes in
var attributes = attributes
attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute })
attributes.append(OutgoingQuickReplyMessageAttribute(shortcut: self.shortcut))
return attributes
}
})
|> deliverOn(self.queue)).startStandalone(next: { [weak self] result in
guard let self else {
return
}
if self.shortcutId != nil {
return
}
for id in result {
if let id {
let pendingMessage = PendingMessageContext()
self.pendingMessages.append(pendingMessage)
pendingMessage.disposable.set((
self.context.account.postbox.messageView(id)
|> deliverOn(self.queue)
).startStrict(next: { [weak self, weak pendingMessage] messageView in
guard let self else {
return
}
guard let pendingMessage else {
return
}
pendingMessage.message = messageView.message
if let message = pendingMessage.message, message.id.namespace == Namespaces.Message.QuickReplyCloud, let threadId = message.threadId {
self.shortcutId = Int32(clamping: threadId)
self.updateHistoryViewRequest(reload: true)
} else {
self.updateHistoryView(updateType: .Generic)
}
}))
}
}
})
}
func deleteMessages(ids: [EngineMessage.Id]) {
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: ids, type: .forEveryone).startStandalone()
}
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
}
func quickReplyUpdateShortcut(value: String) {
self.shortcut = value
if let shortcutId = self.shortcutId {
self.context.engine.accountData.editMessageShortcut(id: shortcutId, shortcut: value)
}
}
}
var kind: ChatCustomContentsKind
var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> {
return self.impl.signalWith({ impl, subscriber in
if let mergedHistoryView = impl.mergedHistoryView {
subscriber.putNext((mergedHistoryView, .Initial))
}
return impl.historyViewStream.signal().start(next: subscriber.putNext)
})
}
var messageLimit: Int? {
return 20
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
init(context: AccountContext, kind: ChatCustomContentsKind, shortcutId: Int32?) {
self.kind = kind
let initialShortcut: String
switch kind {
case let .quickReplyMessageInput(shortcut, _):
initialShortcut = shortcut
case .businessLinkSetup:
initialShortcut = ""
case .hashTagSearch:
initialShortcut = ""
}
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, context: context, shortcut: initialShortcut, shortcutId: shortcutId)
})
}
func enqueueMessages(messages: [EnqueueMessage]) {
self.impl.with { impl in
impl.enqueueMessages(messages: messages)
}
}
func deleteMessages(ids: [EngineMessage.Id]) {
self.impl.with { impl in
impl.deleteMessages(ids: ids)
}
}
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
self.impl.with { impl in
impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
}
}
func quickReplyUpdateShortcut(value: String) {
switch self.kind {
case let .quickReplyMessageInput(_, shortcutType):
self.kind = .quickReplyMessageInput(shortcut: value, shortcutType: shortcutType)
self.impl.with { impl in
impl.quickReplyUpdateShortcut(value: value)
}
case .businessLinkSetup:
break
case .hashTagSearch:
break
}
}
func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) {
}
func loadMore() {
}
func hashtagSearchUpdate(query: String) {
}
var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in }
}
@@ -0,0 +1,117 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import ComponentDisplayAdapters
final class BottomPanelComponent: Component {
let theme: PresentationTheme
let content: AnyComponentWithIdentity<Empty>
let insets: UIEdgeInsets
init(
theme: PresentationTheme,
content: AnyComponentWithIdentity<Empty>,
insets: UIEdgeInsets
) {
self.theme = theme
self.content = content
self.insets = insets
}
static func ==(lhs: BottomPanelComponent, rhs: BottomPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.content != rhs.content {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
final class View: UIView {
private let separatorLayer: SimpleLayer
private let backgroundView: BlurredBackgroundView
private var content = ComponentView<Empty>()
private var component: BottomPanelComponent?
private weak var componentState: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.separatorLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.componentState = state
let themeUpdated = previousComponent?.theme !== component.theme
var contentHeight: CGFloat = 0.0
contentHeight += component.insets.top
var contentTransition = transition
if let previousComponent, previousComponent.content.id != component.content.id {
contentTransition = contentTransition.withAnimation(.none)
self.content.view?.removeFromSuperview()
self.content = ComponentView<Empty>()
}
let contentSize = self.content.update(
transition: contentTransition,
component: component.content.component,
environment: {},
containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height - component.insets.top - component.insets.bottom)
)
let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: contentHeight), size: contentSize)
if let contentView = self.content.view {
if contentView.superview == nil {
self.addSubview(contentView)
}
contentTransition.setFrame(view: contentView, frame: contentFrame)
}
contentHeight += contentSize.height
contentHeight += component.insets.bottom
let size = CGSize(width: availableSize.width, height: contentHeight)
if themeUpdated {
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,96 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
final class BusinessLinkChatContents: ChatCustomContentsProtocol {
private final class Impl {
let queue: Queue
let context: AccountContext
init(queue: Queue, context: AccountContext) {
self.queue = queue
self.context = context
}
deinit {
}
func enqueueMessages(messages: [EnqueueMessage]) {
}
func deleteMessages(ids: [EngineMessage.Id]) {
}
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
}
}
var kind: ChatCustomContentsKind
var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> {
let view = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false)
return .single((view, .Initial))
}
var messageLimit: Int? {
return 20
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
init(context: AccountContext, kind: ChatCustomContentsKind) {
self.kind = kind
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, context: context)
})
}
func enqueueMessages(messages: [EnqueueMessage]) {
self.impl.with { impl in
impl.enqueueMessages(messages: messages)
}
}
func deleteMessages(ids: [EngineMessage.Id]) {
self.impl.with { impl in
impl.deleteMessages(ids: ids)
}
}
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
self.impl.with { impl in
impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
}
}
func quickReplyUpdateShortcut(value: String) {
}
func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) {
if case let .businessLinkSetup(link) = self.kind {
self.kind = .businessLinkSetup(link: TelegramBusinessChatLinks.Link(
url: link.url,
message: message,
entities: entities,
title: title,
viewCount: link.viewCount
))
}
}
func loadMore() {
}
func hashtagSearchUpdate(query: String) {
}
var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in }
}
@@ -0,0 +1,341 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ListSectionComponent
import TelegramPresentationData
import AppBundle
import AccountContext
import Postbox
import TelegramCore
import TextNodeWithEntities
import MultilineTextComponent
import TextFormat
import ListItemSwipeOptionContainer
final class BusinessLinkListItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let link: TelegramBusinessChatLinks.Link
let action: () -> Void
let deleteAction: () -> Void
let shareAction: () -> Void
let contextAction: ((ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
link: TelegramBusinessChatLinks.Link,
action: @escaping () -> Void,
deleteAction: @escaping () -> Void,
shareAction: @escaping () -> Void,
contextAction: ((ContextExtractedContentContainingView, ContextGesture) -> Void)?
) {
self.context = context
self.theme = theme
self.strings = strings
self.link = link
self.action = action
self.deleteAction = deleteAction
self.shareAction = shareAction
self.contextAction = contextAction
}
static func ==(lhs: BusinessLinkListItemComponent, rhs: BusinessLinkListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.link != rhs.link {
return false
}
return true
}
final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
private let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton
private let swipeOptionContainer: ListItemSwipeOptionContainer
private let iconView = UIImageView()
private let viewCount = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = TextNodeWithEntities()
private var component: BusinessLinkListItemComponent?
private weak var componentState: EmptyComponentState?
var customUpdateIsHighlighted: ((Bool) -> Void)?
var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
private var isExtractedToContextMenu: Bool = false
override init(frame: CGRect) {
self.extractedContainerView = ContextExtractedContentContainingView()
self.containerButton = HighlightTrackingButton()
self.containerButton.layer.anchorPoint = CGPoint()
self.containerButton.isExclusiveTouch = true
self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect())
super.init(frame: frame)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.containerButton.internalHighligthedChanged = { [weak self] isHighlighted in
guard let self else {
return
}
if let customUpdateIsHighlighted = self.customUpdateIsHighlighted {
customUpdateIsHighlighted(isHighlighted)
}
}
self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in
guard let self else {
return
}
transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size))
}
self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in
guard let self, let component = self.component else {
return
}
self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true)
if option.key == AnyHashable(0 as Int) {
component.shareAction()
} else {
component.deleteAction()
}
}
self.swipeOptionContainer.addSubview(self.containerButton)
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
guard let self, let component = self.component else {
return
}
self.containerButton.clipsToBounds = value
self.containerButton.backgroundColor = value ? component.theme.list.itemBlocksBackgroundColor : nil
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
}
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
guard let self else {
return
}
self.isExtractedToContextMenu = value
let mappedTransition: ComponentTransition
if value {
mappedTransition = ComponentTransition(transition)
} else {
mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
}
self.componentState?.updated(transition: mappedTransition)
}
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
func update(component: BusinessLinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
let _ = previousComponent
self.component = component
self.componentState = state
let leftInset: CGFloat = 0.0
let leftContentInset: CGFloat = 62.0
var rightInset: CGFloat = 8.0
let topInset: CGFloat = 9.0
let bottomInset: CGFloat = 9.0
let titleViewCountSpacing: CGFloat = 4.0
let titleTextSpacing: CGFloat = 4.0
var innerInsets = UIEdgeInsets()
if self.isExtractedToContextMenu {
rightInset += 2.0
innerInsets.left += 2.0
innerInsets.right += 2.0
}
let viewCountText: String
if component.link.viewCount == 0 {
viewCountText = component.strings.Business_Links_ItemNoClicks
} else {
viewCountText = component.strings.Business_Links_ItemClickCount(Int32(component.link.viewCount))
}
let viewCountSize = self.viewCount.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: viewCountText, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let viewCountFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - innerInsets.left - viewCountSize.width, y: topInset + 2.0), size: viewCountSize)
if let viewCountView = self.viewCount.view {
if viewCountView.superview == nil {
viewCountView.isUserInteractionEnabled = false
viewCountView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
self.containerButton.addSubview(viewCountView)
}
transition.setPosition(view: viewCountView, position: CGPoint(x: viewCountFrame.maxX, y: viewCountFrame.minY))
viewCountView.bounds = CGRect(origin: CGPoint(), size: viewCountFrame.size)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.link.title ?? component.link.url, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - leftContentInset - rightInset - viewCountSize.width - titleViewCountSpacing, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: leftInset + leftContentInset, y: topInset), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
titleView.layer.anchorPoint = CGPoint()
self.containerButton.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let asyncLayout = TextNodeWithEntities.asyncLayout(self.text)
let filteredEntities = component.link.entities.filter { entity in
switch entity.type {
case .CustomEmoji:
return true
default:
return false
}
}
let textString = stringWithAppliedEntities(
component.link.message.isEmpty ? component.strings.Business_Links_ItemNoText : component.link.message,
entities: filteredEntities,
baseColor: component.theme.list.itemSecondaryTextColor,
linkColor: component.theme.list.itemSecondaryTextColor,
baseQuoteTintColor: nil,
baseQuoteSecondaryTintColor: nil,
baseQuoteTertiaryTintColor: nil,
codeBlockTitleColor: nil,
codeBlockAccentColor: nil,
codeBlockBackgroundColor: nil,
baseFont: Font.regular(15.0),
linkFont: Font.regular(15.0),
boldFont: Font.semibold(15.0),
italicFont: Font.italic(15.0),
boldItalicFont: Font.semiboldItalic(15.0),
fixedFont: Font.monospace(15.0),
blockQuoteFont: Font.regular(15.0),
underlineLinks: false,
external: false,
message: nil,
entityFiles: [:],
adjustQuoteFontSize: false,
cachedMessageSyntaxHighlight: nil
)
let (textLayout, textApply) = asyncLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: availableSize.width - leftContentInset - leftInset - rightInset, height: 100.0)))
let _ = textApply(TextNodeWithEntities.Arguments(
context: component.context,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
placeholderColor: component.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
))
let textSize = textLayout.size
let textFrame = CGRect(origin: CGPoint(x: leftInset + leftContentInset, y: titleFrame.maxY + titleTextSpacing), size: textLayout.size)
if self.text.textNode.view.superview == nil {
self.text.textNode.view.isUserInteractionEnabled = false
self.containerButton.addSubview(self.text.textNode.view)
}
transition.setFrame(view: self.text.textNode.view, frame: textFrame)
let size = CGSize(width: availableSize.width, height: topInset + titleSize.height + titleTextSpacing + textSize.height + bottomInset)
self.iconView.image = PresentationResourcesItemList.sharedLinkIcon(component.theme)
if let image = self.iconView.image {
if self.iconView.superview == nil {
self.iconView.isUserInteractionEnabled = false
self.containerButton.addSubview(self.iconView)
}
let iconFrame = CGRect(origin: CGPoint(x: leftInset + floor((leftContentInset - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)
transition.setFrame(view: self.iconView, frame: iconFrame)
}
let swipeOptionContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame)
let containerButtonFrame = CGRect(origin: CGPoint(x: innerInsets.left, y: innerInsets.top), size: CGSize(width: size.width - innerInsets.left - innerInsets.right, height: size.height - innerInsets.top - innerInsets.bottom))
transition.setPosition(view: self.containerButton, position: containerButtonFrame.origin)
transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerButtonFrame.size))
self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0)
let resultBounds = CGRect(origin: CGPoint(), size: size)
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
self.extractedContainerView.contentRect = resultBounds
var rightOptions: [ListItemSwipeOptionContainer.Option] = []
rightOptions = [
ListItemSwipeOptionContainer.Option(
key: 0,
title: component.strings.Business_Links_ItemActionShare,
icon: .none,
color: component.theme.list.itemDisclosureActions.accent.fillColor,
textColor: component.theme.list.itemDisclosureActions.accent.foregroundColor
),
ListItemSwipeOptionContainer.Option(
key: 1,
title: component.strings.Common_Delete,
icon: .none,
color: component.theme.list.itemDisclosureActions.destructive.fillColor,
textColor: component.theme.list.itemDisclosureActions.destructive.foregroundColor
)
]
self.swipeOptionContainer.setRevealOptions(([], rightOptions))
self.separatorInset = leftContentInset
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,783 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ListSectionComponent
import TelegramPresentationData
import AppBundle
import AccountContext
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import LottieComponent
import Markdown
import SwiftSignalKit
import TelegramCore
import ListActionItemComponent
import BundleIconComponent
import TextFormat
import UndoUI
import ShareController
import ContextUI
final class BusinessLinksSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let initialData: BusinessLinksSetupScreen.InitialData
init(
context: AccountContext,
initialData: BusinessLinksSetupScreen.InitialData
) {
self.context = context
self.initialData = initialData
}
static func ==(lhs: BusinessLinksSetupScreenComponent, rhs: BusinessLinksSetupScreenComponent) -> 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 icon = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let createLinkSection = ComponentView<Empty>()
private let linksSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: BusinessLinksSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var refreshLinksDisposable: Disposable?
private var links: [TelegramBusinessChatLinks.Link] = []
private var linksDisposable: Disposable?
private var isCreatingLink: Bool = false
private var createLinkDisposable: 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.refreshLinksDisposable?.dispose()
self.linksDisposable?.dispose()
self.createLinkDisposable?.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: ComponentTransition) {
let navigationRevealOffsetY: CGFloat = 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: 1.0)
}
}
private func createLink() {
guard let component = self.component else {
return
}
if self.isCreatingLink {
return
}
self.isCreatingLink = true
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
self.createLinkDisposable?.dispose()
self.createLinkDisposable = (component.context.engine.accountData.createBusinessChatLink(message: "", entities: [], title: nil)
|> deliverOnMainQueue).startStrict(next: { [weak self] link in
guard let self else {
return
}
self.isCreatingLink = false
self.state?.updated(transition: .immediate)
self.openLink(link: link, openKeyboard: true)
}, error: { [weak self] error in
guard let self, let component = self.component, let environment = self.environment else {
return
}
self.isCreatingLink = false
self.state?.updated(transition: .immediate)
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let errorText: String
switch error {
case .generic:
errorText = presentationData.strings.Login_UnknownError
case .tooManyLinks:
errorText = presentationData.strings.Business_Links_ErrorTooManyLinks
}
environment.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: errorText, actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
})
]), in: .window(.root))
})
}
private func openLink(url: String) {
if let link = self.links.first(where: { $0.url == url }) {
self.openLink(link: link, openKeyboard: false)
}
}
private func openLink(link: TelegramBusinessChatLinks.Link, openKeyboard: Bool) {
guard let component = self.component else {
return
}
let contents = BusinessLinkChatContents(
context: component.context,
kind: .businessLinkSetup(link: link)
)
let chatController = component.context.sharedContext.makeChatController(
context: component.context,
chatLocation: .customChatContents,
subject: .customChatContents(contents: contents),
botStart: nil,
mode: .standard(.default),
params: nil
)
if openKeyboard {
chatController.activateInput(type: .text)
}
chatController.navigationPresentation = .modal
self.environment?.controller()?.push(chatController)
}
private func openDeleteLink(url: String) {
guard let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Business_Links_DeleteItemConfirmationAction, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.accountData.deleteBusinessChatLink(url: url).startStandalone()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
self.environment?.controller()?.present(actionSheet, in: .window(.root))
}
private func openShareLink(url: String) {
guard let component = self.component, let environment = self.environment else {
return
}
guard let link = self.links.first(where: { $0.url == url }) else {
return
}
environment.controller()?.present(ShareController(context: component.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), in: .window(.root))
}
func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.links = component.initialData.businessLinks?.links ?? []
self.linksDisposable = (component.context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.BusinessChatLinks(id: component.context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] links in
guard let self else {
return
}
let links = links?.links ?? []
if self.links != links {
self.links = links
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}
})
self.refreshLinksDisposable = component.context.engine.accountData.refreshBusinessChatLinks().startStrict()
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.25)
} else {
alphaTransition = .immediate
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let _ = alphaTransition
let _ = presentationData
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Business_Links, 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 = 24.0
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let _ = sectionSpacing
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "MessageLinkEmoji"),
loop: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize)
if let iconView = self.icon.view as? LottieComponent.View {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
iconView.playOnce()
}
transition.setPosition(view: iconView, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Business_Links_Text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { attributes in
return ("URL", "")
}), textAlignment: .center
))
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
text: .plain(subtitleString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.25,
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
},
tapAction: { _, _ in
}
)),
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 += 27.0
var createLinkSectionItems: [AnyComponentWithIdentity<Empty>] = []
createLinkSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Business_Links_CreateAction,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Item List/AddLinkIcon",
tintColor: environment.theme.list.itemAccentColor
))), false),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.createLink()
}
))))
let footerText: String
if let addressName = component.initialData.accountPeer?.addressName, let phoneNumber = component.initialData.accountPeer?.phone, component.initialData.displayPhone {
footerText = environment.strings.Business_Links_SimpleLinkInfoUsernamePhone(addressName, phoneNumber).string
} else if let addressName = component.initialData.accountPeer?.addressName {
footerText = environment.strings.Business_Links_SimpleLinkInfoUsername(addressName).string
} else if let phoneNumber = component.initialData.accountPeer?.phone, component.initialData.displayPhone {
footerText = environment.strings.Business_Links_SimpleLinkInfoPhone(phoneNumber).string
} else {
footerText = ""
}
let createLinkSectionSize = self.createLinkSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
header: nil,
footer: footerText.isEmpty ? nil : AnyComponent(MultilineTextComponent(
text: .markdown(text: footerText, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { link in
return ("URL", link)
}
)),
maximumNumberOfLines: 0,
lineSpacing: 0.25,
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
},
tapAction: { [weak self] attributes, _ in
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let url = attributes[NSAttributedString.Key(rawValue: "URL")] as? String else {
return
}
let linkValue: String
if url == "phone", let phoneNumber = component.initialData.accountPeer?.phone {
linkValue = "t.me/+\(phoneNumber)"
} else if url == "username", let addressName = component.initialData.accountPeer?.addressName {
linkValue = "t.me/\(addressName)"
} else {
return
}
UIPasteboard.general.string = linkValue
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
var animateAsReplacement = false
if let controller = environment.controller() {
controller.forEachController { c in
if let c = c as? UndoOverlayController {
animateAsReplacement = true
c.dismiss()
}
return true
}
}
let controller = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.GroupInfo_InviteLink_CopyAlert_Success), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in
return false
})
environment.controller()?.present(controller, in: .current)
}
)),
items: createLinkSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let createLinkSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: createLinkSectionSize)
if let createLinkSectionView = self.createLinkSection.view {
if createLinkSectionView.superview == nil {
self.scrollView.addSubview(createLinkSectionView)
self.createLinkSection.parentState = state
}
transition.setFrame(view: createLinkSectionView, frame: createLinkSectionFrame)
}
contentHeight += createLinkSectionSize.height
contentHeight += sectionSpacing
var linksSectionItems: [AnyComponentWithIdentity<Empty>] = []
for link in self.links {
linksSectionItems.append(AnyComponentWithIdentity(id: link.url, component: AnyComponent(BusinessLinkListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
link: link,
action: { [weak self] in
guard let self else {
return
}
self.openLink(url: link.url)
},
deleteAction: { [weak self] in
guard let self else {
return
}
self.openDeleteLink(url: link.url)
},
shareAction: { [weak self] in
guard let self else {
return
}
self.openShareLink(url: link.url)
},
contextAction: { [weak self] sourceView, gesture in
guard let self, let component = self.component else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var itemList: [ContextMenuItem] = []
itemList.append(.action(ContextMenuActionItem(
text: presentationData.strings.Business_Links_ItemActionShare,
textColor: .primary,
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) },
action: { [weak self] c, _ in
c?.dismiss(completion: {
guard let self else {
return
}
self.openShareLink(url: link.url)
})
})
))
itemList.append(.action(ContextMenuActionItem(
text: presentationData.strings.Common_Delete,
textColor: .destructive,
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) },
action: { [weak self] c, _ in
c?.dismiss(completion: {
guard let self else {
return
}
self.openDeleteLink(url: link.url)
})
})
))
let items = ContextController.Items(content: .list(itemList))
let controller = ContextController(
presentationData: presentationData,
source: .extracted(BusineesLinkListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)
self.environment?.controller()?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
self.environment?.controller()?.presentInGlobalOverlay(controller)
}
))))
}
let linksSectionSize = self.linksSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Business_Links_LinksSectionHeader,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: linksSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let linksSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linksSectionSize)
if let linksSectionView = self.linksSection.view {
if linksSectionView.superview == nil {
self.scrollView.addSubview(linksSectionView)
self.linksSection.parentState = state
}
transition.setFrame(view: linksSectionView, frame: linksSectionFrame)
alphaTransition.setAlpha(view: linksSectionView, alpha: self.links.isEmpty ? 0.0 : 1.0)
}
contentHeight += linksSectionSize.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.verticalScrollIndicatorInsets != scrollInsets {
self.scrollView.verticalScrollIndicatorInsets = 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: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class BusinessLinksSetupScreen: ViewControllerComponentContainer {
public final class InitialData: BusinessLinksSetupScreenInitialData {
fileprivate let accountPeer: TelegramUser?
fileprivate let businessLinks: TelegramBusinessChatLinks?
fileprivate let displayPhone: Bool
fileprivate init(accountPeer: TelegramUser?, businessLinks: TelegramBusinessChatLinks?, displayPhone: Bool) {
self.accountPeer = accountPeer
self.businessLinks = businessLinks
self.displayPhone = displayPhone
}
}
private let context: AccountContext
public init(
context: AccountContext,
initialData: InitialData
) {
self.context = context
super.init(context: context, component: BusinessLinksSetupScreenComponent(
context: context,
initialData: initialData
), 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? BusinessLinksSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? BusinessLinksSetupScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public static func makeInitialData(context: AccountContext) -> Signal<BusinessLinksSetupScreenInitialData, NoError> {
let settingsPromise: Promise<AccountPrivacySettings?>
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() {
settingsPromise = current
} else {
settingsPromise = Promise()
settingsPromise.set(.single(nil))
}
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.BusinessChatLinks(id: context.account.peerId)
),
settingsPromise.get()
|> take(1)
)
|> map { data, settings in
let (peer, businessLinks) = data
var accountPeer: TelegramUser?
if case let .user(user) = peer {
accountPeer = user
}
var displayPhone = true
if let settings {
displayPhone = settings.phoneDiscoveryEnabled
}
return InitialData(
accountPeer: accountPeer,
businessLinks: businessLinks,
displayPhone: displayPhone
)
}
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}
private final class BusineesLinkListContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = false
let blurBackground: Bool = true
private let contentView: ContextExtractedContentContainingView
init(contentView: ContextExtractedContentContainingView) {
self.contentView = contentView
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
@@ -0,0 +1,186 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import AppBundle
import ButtonComponent
import MultilineTextComponent
import BalancedTextComponent
import LottieComponent
final class QuickReplyEmptyStateComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let insets: UIEdgeInsets
let action: () -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
insets: UIEdgeInsets,
action: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.insets = insets
self.action = action
}
static func ==(lhs: QuickReplyEmptyStateComponent, rhs: QuickReplyEmptyStateComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var component: QuickReplyEmptyStateComponent?
private weak var componentState: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: QuickReplyEmptyStateComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.componentState = state
let _ = previousComponent
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: component.strings.QuickReplies_EmptyState_AddButton,
badge: 0,
textColor: component.theme.list.itemCheckColors.foregroundColor,
badgeBackground: component.theme.list.itemCheckColors.foregroundColor,
badgeForeground: component.theme.list.itemCheckColors.fillColor
))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action()
}
)),
environment: {},
containerSize: CGSize(width: min(availableSize.width - 16.0 * 2.0, 280.0), height: 50.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: availableSize.height - component.insets.bottom - 14.0 - buttonSize.height), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
let iconTitleSpacing: CGFloat = 13.0
let titleTextSpacing: CGFloat = 9.0
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "WriteEmoji"),
loop: false
)),
environment: {},
containerSize: CGSize(width: 120.0, height: 120.0)
)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.QuickReplies_EmptyState_Title, font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: component.strings.QuickReplies_EmptyState_Text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 20,
lineSpacing: 0.2
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 100.0)
)
let topInset: CGFloat = component.insets.top
let centralContentsHeight: CGFloat = iconSize.height + iconTitleSpacing + titleSize.height + titleTextSpacing
var centralContentsY: CGFloat = topInset + floor((buttonFrame.minY - topInset - centralContentsHeight) * 0.426)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: centralContentsY), size: iconSize)
if let iconView = self.icon.view as? LottieComponent.View {
if iconView.superview == nil {
self.addSubview(iconView)
iconView.playOnce()
}
transition.setFrame(view: iconView, frame: iconFrame)
}
centralContentsY += iconSize.height + iconTitleSpacing
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: centralContentsY), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setPosition(view: titleView, position: titleFrame.center)
}
centralContentsY += titleSize.height + titleTextSpacing
let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: centralContentsY), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
transition.setPosition(view: textView, position: textFrame.center)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}