Merge commit '863c4afb910d4b23c50f802fccbcdd3ffcef7846' into experimental-3

# Conflicts:
#	submodules/TelegramApi/Sources/Api31.swift
This commit is contained in:
Ali 2023-08-27 17:56:03 +04:00
commit c0d2dc07e5
29 changed files with 9905 additions and 208 deletions

View File

@ -9861,3 +9861,5 @@ Sorry for the inconvenience.";
"Story.ViewList.ViewerCount_1" = "1 Viewer";
"Story.ViewList.ViewerCount_any" = "%d Viewers";
"AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@.";

View File

@ -1242,6 +1242,8 @@ private final class DrawingScreenComponent: CombinedComponent {
nextStyle = .regular
case .stroke:
nextStyle = .regular
case .blur:
nextStyle = .regular
}
textEntity.style = nextStyle
updateEntityView.invoke((textEntity.uuid, false))
@ -3515,6 +3517,8 @@ public final class DrawingToolsInteraction {
nextStyle = .regular
case .stroke:
nextStyle = .regular
case .blur:
nextStyle = .regular
}
textEntity.style = nextStyle
entityView.update()

View File

@ -48,6 +48,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
if case .file(_, .reaction) = entity.content {
let backgroundNode = ASImageNode()
backgroundNode.layer.zPosition = -1000.0
backgroundNode.image = UIImage(bundleImageName: "Media Editor/ReactionBackground")
backgroundNode.displaysAsynchronously = false
self.addSubnode(backgroundNode)
@ -319,15 +320,13 @@ public final class DrawingStickerEntityView: DrawingEntityView {
var boundingSize = CGSize(width: sideSize, height: sideSize)
if let backgroundNode = self.backgroundNode {
backgroundNode.frame = CGRect(origin: .zero, size: boundingSize)
backgroundNode.frame = CGRect(origin: .zero, size: boundingSize).insetBy(dx: -5.0, dy: -5.0)
boundingSize = CGSize(width: floor(sideSize * 0.63), height: floor(sideSize * 0.63))
}
let imageSize = self.dimensions.aspectFitted(boundingSize)
var imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
imageFrame = imageFrame.offsetBy(dx: -3.0, dy: -9.0)
}
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = imageFrame
if let animationNode = self.animationNode {
@ -351,6 +350,24 @@ public final class DrawingStickerEntityView: DrawingEntityView {
}
}
private var isReaction: Bool {
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
return true
} else {
return false
}
}
override func animateInsertion() {
super.animateInsertion()
if self.isReaction {
Queue.mainQueue().after(0.2) {
let _ = self.selectedTapAction()
}
}
}
func onDeselection() {
let _ = self.dismissReactionSelection()
}
@ -560,10 +577,14 @@ public final class DrawingStickerEntityView: DrawingEntityView {
self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size))
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale)
let isReaction = self.isReaction
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
if animated {
let isCurrentlyMirrored = ((self.imageNode.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
var isCurrentlyMirrored = ((self.imageNode.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
if isReaction {
isCurrentlyMirrored = ((self.backgroundNode?.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
}
var animationSourceTransform = CATransform3DIdentity
var animationTargetTransform = CATransform3DIdentity
if isCurrentlyMirrored {
@ -574,23 +595,44 @@ public final class DrawingStickerEntityView: DrawingEntityView {
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
}
self.imageNode.transform = animationSourceTransform
self.animationNode?.transform = animationSourceTransform
if isReaction {
self.backgroundNode?.transform = animationSourceTransform
let values = [1.0, 0.01, 1.0]
let keyTimes = [0.0, 0.5, 1.0]
self.animationNode?.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.25, keyPath: "transform.scale.x", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
} else {
self.imageNode.transform = animationSourceTransform
self.animationNode?.transform = animationSourceTransform
self.videoNode?.transform = animationSourceTransform
}
UIView.animate(withDuration: 0.25, animations: {
self.imageNode.transform = animationTargetTransform
self.animationNode?.transform = animationTargetTransform
self.videoNode?.transform = animationTargetTransform
if isReaction {
self.backgroundNode?.transform = animationTargetTransform
} else {
self.imageNode.transform = animationTargetTransform
self.animationNode?.transform = animationTargetTransform
self.videoNode?.transform = animationTargetTransform
}
}, completion: { finished in
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoNode?.transform = staticTransform
if isReaction {
self.backgroundNode?.transform = staticTransform
} else {
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoNode?.transform = staticTransform
}
})
} else {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoNode?.transform = staticTransform
if isReaction {
self.backgroundNode?.transform = staticTransform
} else {
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoNode?.transform = staticTransform
}
CATransaction.commit()
}
@ -607,12 +649,9 @@ public final class DrawingStickerEntityView: DrawingEntityView {
selectionView.transform = .identity
let maxSide = max(self.selectionBounds.width, self.selectionBounds.height)
var center = self.selectionBounds.center
let center = self.selectionBounds.center
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
center = center.offsetBy(dx: -8.0 * scale, dy: -18.0 * scale)
}
selectionView.center = self.convert(center, to: selectionView.superview)

View File

@ -27,6 +27,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
return self.entity as! DrawingTextEntity
}
let blurredBackgroundView: BlurredBackgroundView
let textView: DrawingTextView
var customEmojiContainerView: CustomEmojiContainerView?
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
@ -35,6 +36,10 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in }
init(context: AccountContext, entity: DrawingTextEntity) {
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
self.blurredBackgroundView.clipsToBounds = true
self.blurredBackgroundView.isHidden = true
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
@ -56,6 +61,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
super.init(context: context, entity: entity)
self.textView.delegate = self
self.addSubview(self.blurredBackgroundView)
self.addSubview(self.textView)
self.emojiViewProvider = { emoji in
@ -174,6 +180,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
textColor = color
case .stroke:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
case .blur:
textColor = color
}
self.emojiRects = customEmojiRects
@ -466,6 +474,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
textColor = color
case .stroke:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
case .blur:
textColor = color
}
guard let visualText = text.mutableCopy() as? NSMutableAttributedString else {
@ -629,6 +639,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
self.textView.strokeColor = color
self.textView.frameColor = nil
case .blur:
break
}
self.textView.tintColor = self.textView.text.isEmpty ? .white : cursorColor

View File

@ -1527,7 +1527,7 @@ public class StickerPickerScreen: ViewController {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let (layout, _) = self.currentLayout {
if case .regular = layout.metrics.widthClass {
if layout.metrics.isTablet {
return false
}
}
@ -1586,10 +1586,7 @@ public class StickerPickerScreen: ViewController {
self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
var effectiveExpanded = self.isExpanded
if case .regular = layout.metrics.widthClass {
effectiveExpanded = true
}
let effectiveExpanded = self.isExpanded || layout.metrics.isTablet
let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset

View File

@ -12,6 +12,7 @@ enum DrawingTextStyle: Equatable {
case filled
case semi
case stroke
case blur
init(style: DrawingTextEntity.Style) {
switch style {
@ -23,6 +24,8 @@ enum DrawingTextStyle: Equatable {
self = .semi
case .stroke:
self = .stroke
case .blur:
self = .blur
}
}
}
@ -502,6 +505,8 @@ final class TextSettingsComponent: CombinedComponent {
styleImage = state.image(.semi)
case .stroke:
styleImage = state.image(.stroke)
case .blur:
styleImage = state.image(.stroke)
}
var fontAvailableWidth: CGFloat = context.availableSize.width

View File

@ -455,11 +455,11 @@ final class MediaPickerGridItemNode: GridItemNode {
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
}
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|> then(
assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|> delay(0.03, queue: Queue.concurrentDefaultQueue())
)
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false)
// |> then(
// assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
// |> delay(0.03, queue: Queue.concurrentDefaultQueue())
// )
if stories {
self.imageNode.contentUpdated = { [weak self] image in

View File

@ -676,68 +676,70 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if !controller.didSetupGroups {
controller.didSetupGroups = true
controller.groupsPromise.set(
combineLatest(
self.mediaAssetsContext.fetchAssetsCollections(.album),
self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
)
|> map { albums, smartAlbums -> [MediaGroupItem] in
var collections: [PHAssetCollection] = []
smartAlbums.enumerateObjects { collection, _, _ in
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
collections.append(collection)
Queue.concurrentDefaultQueue().after(0.3) {
controller.groupsPromise.set(
combineLatest(
self.mediaAssetsContext.fetchAssetsCollections(.album),
self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
)
|> map { albums, smartAlbums -> [MediaGroupItem] in
var collections: [PHAssetCollection] = []
smartAlbums.enumerateObjects { collection, _, _ in
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
collections.append(collection)
}
}
}
smartAlbums.enumerateObjects { collection, index, _ in
var supportedAlbums: [PHAssetCollectionSubtype] = [
.smartAlbumBursts,
.smartAlbumPanoramas,
.smartAlbumScreenshots,
.smartAlbumSelfPortraits,
.smartAlbumSlomoVideos,
.smartAlbumTimelapses,
.smartAlbumVideos,
.smartAlbumAllHidden
]
if #available(iOS 11, *) {
supportedAlbums.append(.smartAlbumAnimated)
supportedAlbums.append(.smartAlbumDepthEffect)
supportedAlbums.append(.smartAlbumLivePhotos)
smartAlbums.enumerateObjects { collection, index, _ in
var supportedAlbums: [PHAssetCollectionSubtype] = [
.smartAlbumBursts,
.smartAlbumPanoramas,
.smartAlbumScreenshots,
.smartAlbumSelfPortraits,
.smartAlbumSlomoVideos,
.smartAlbumTimelapses,
.smartAlbumVideos,
.smartAlbumAllHidden
]
if #available(iOS 11, *) {
supportedAlbums.append(.smartAlbumAnimated)
supportedAlbums.append(.smartAlbumDepthEffect)
supportedAlbums.append(.smartAlbumLivePhotos)
}
if supportedAlbums.contains(collection.assetCollectionSubtype) {
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
collections.append(collection)
}
}
}
if supportedAlbums.contains(collection.assetCollectionSubtype) {
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
collections.append(collection)
}
}
}
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
collections.append(collection)
}
}
var items: [MediaGroupItem] = []
for collection in collections {
let result = PHAsset.fetchAssets(in: collection, options: nil)
let firstItem: PHAsset?
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
firstItem = result.lastObject
} else {
firstItem = result.firstObject
}
items.append(
MediaGroupItem(
collection: collection,
firstItem: firstItem,
count: result.count
var items: [MediaGroupItem] = []
for collection in collections {
let result = PHAsset.fetchAssets(in: collection, options: nil)
let firstItem: PHAsset?
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
firstItem = result.lastObject
} else {
firstItem = result.firstObject
}
items.append(
MediaGroupItem(
collection: collection,
firstItem: firstItem,
count: result.count
)
)
)
}
return items
}
return items
}
)
)
}
}
} else if case .notDetermined = mediaAccess, !self.requestedMediaAccess {
self.requestedMediaAccess = true

File diff suppressed because it is too large Load Diff

View File

@ -50,8 +50,20 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .historyScreenshot)
case let .messageActionCustomAction(message):
return TelegramMediaAction(action: .customText(text: message, entities: []))
case let .messageActionBotAllowed(_, domain, _):
return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain ?? ""))
case let .messageActionBotAllowed(flags, domain, app):
if let domain = domain {
return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain))
} else if case let .botApp(_, _, _, _, appName, _, _, _, _) = app {
var type: BotSendMessageAccessGrantedType?
if (flags & (1 << 3)) != 0 {
type = .request
} else if (flags & (1 << 1)) != 0 {
type = .attachMenu
}
return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type))
} else {
return nil
}
case .messageActionSecureValuesSentMe:
return nil
case let .messageActionSecureValuesSent(types):

View File

@ -23,6 +23,11 @@ public enum SentSecureValueType: Int32 {
case temporaryRegistration = 12
}
public enum BotSendMessageAccessGrantedType: Int32 {
case attachMenu = 0
case request = 1
}
public enum TelegramMediaActionType: PostboxCoding, Equatable {
public enum ForumTopicEditComponent: PostboxCoding, Equatable {
case title(String)
@ -86,6 +91,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool)
case customText(text: String, entities: [MessageTextEntity])
case botDomainAccessGranted(domain: String)
case botAppAccessGranted(appName: String, type: BotSendMessageAccessGrantedType?)
case botSentSecureValues(types: [SentSecureValueType])
case peerJoined
case phoneNumberRequest
@ -195,6 +201,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
self = .unknown
}
case 35:
self = .botAppAccessGranted(appName: decoder.decodeStringForKey("app", orElse: ""), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
default:
self = .unknown
}
@ -362,6 +370,14 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .setSameChatWallpaper(wallpaper):
encoder.encodeInt32(34, forKey: "_rawValue")
encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper")
case let .botAppAccessGranted(appName, type):
encoder.encodeInt32(35, forKey: "_rawValue")
encoder.encodeString(appName, forKey: "app")
if let type = type {
encoder.encodeInt32(type.rawValue, forKey: "atp")
} else {
encoder.encodeNil(forKey: "atp")
}
}
}

View File

@ -236,3 +236,72 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage
|> castError(RequestAppWebViewError.self)
|> switchToLatest
}
func _internal_canBotSendMessages(postbox: Postbox, network: Network, botId: PeerId) -> Signal<Bool, NoError> {
return postbox.transaction { transaction -> Signal<Bool, NoError> in
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
return .single(false)
}
return network.request(Api.functions.bots.canSendMessage(bot: inputUser))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> map { result -> Bool in
if case .boolTrue = result {
return true
} else {
return false
}
}
}
|> switchToLatest
}
func _internal_allowBotSendMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, botId: PeerId) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Signal<Never, NoError> in
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
return .never()
}
return network.request(Api.functions.bots.allowSendMessage(bot: inputUser))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> map { updates -> Api.Updates? in
if let updates = updates {
stateManager.addUpdates(updates)
}
return updates
}
|> ignoreValues
}
|> switchToLatest
}
public enum InvokeBotCustomMethodError {
case generic
}
func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId: PeerId, method: String, params: String) -> Signal<String, InvokeBotCustomMethodError> {
let params = Api.DataJSON.dataJSON(data: params)
return postbox.transaction { transaction -> Signal<String, InvokeBotCustomMethodError> in
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
return .fail(.generic)
}
return network.request(Api.functions.bots.invokeWebViewCustomMethod(bot: inputUser, customMethod: method, params: params))
|> mapError { _ -> InvokeBotCustomMethodError in
return .generic
}
|> map { result -> String in
if case let .dataJSON(data) = result {
return data
} else {
return ""
}
}
}
|> castError(InvokeBotCustomMethodError.self)
|> switchToLatest
}

View File

@ -507,6 +507,18 @@ public extension TelegramEngine {
public func sendWebViewData(botId: PeerId, buttonText: String, data: String) -> Signal<Never, SendWebViewDataError> {
return _internal_sendWebViewData(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId, buttonText: buttonText, data: data)
}
public func canBotSendMessages(botId: PeerId) -> Signal<Bool, NoError> {
return _internal_canBotSendMessages(postbox: self.account.postbox, network: self.account.network, botId: botId)
}
public func allowBotSendMessages(botId: PeerId) -> Signal<Never, NoError> {
return _internal_allowBotSendMessages(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId)
}
public func invokeBotCustomMethod(botId: PeerId, method: String, params: String) -> Signal<String, InvokeBotCustomMethodError> {
return _internal_invokeBotCustomMethod(postbox: self.account.postbox, network: self.account.network, botId: botId, method: method, params: params)
}
public func addBotToAttachMenu(botId: PeerId, allowWrite: Bool) -> Signal<Bool, AddBotToAttachMenuError> {
return _internal_addBotToAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, allowWrite: allowWrite)

View File

@ -629,6 +629,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())
case let .botDomainAccessGranted(domain):
attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor)
case let .botAppAccessGranted(appName, _):
attributedString = NSAttributedString(string: strings.AuthSessions_MessageApp(appName).string, font: titleFont, textColor: primaryTextColor)
case let .botSentSecureValues(types):
var typesString = ""
var hasIdentity = false

View File

@ -1757,7 +1757,7 @@ public class CameraScreen: ViewController {
self.backgroundView.alpha = 1.0
})
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
self.controller?.statusBar.updateStatusBarStyle(.Hide, animated: true)
}
@ -2075,12 +2075,7 @@ public class CameraScreen: ViewController {
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet: Bool
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let isTablet = layout.metrics.isTablet
var topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
let previewSize: CGSize
@ -2615,7 +2610,7 @@ public class CameraScreen: ViewController {
self.node.camera?.stopCapture(invalidate: true)
self.isDismissed = true
if animated {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
self.node.animateOut(completion: {
self.dismiss(animated: false)
@ -2637,7 +2632,7 @@ public class CameraScreen: ViewController {
}
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
return
}
@ -2683,7 +2678,7 @@ public class CameraScreen: ViewController {
}
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
return
}
if dismissing {

View File

@ -80,7 +80,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
public var referenceDrawingSize: CGSize
public var position: CGPoint
public var scale: CGFloat
public var scale: CGFloat {
didSet {
if case let .file(_, type) = self.content, case .reaction = type {
self.scale = max(0.75, min(2.0, self.scale))
}
}
}
public var rotation: CGFloat
public var mirrored: Bool

View File

@ -61,6 +61,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
case filled
case semi
case stroke
case blur
}
public enum Animation: Codable, Equatable {

View File

@ -63,8 +63,9 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage,
func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] {
if let entity = entity as? DrawingStickerEntity {
if case let .file(_, type) = entity.content, case .reaction = type, let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)]
if case let .file(_, type) = entity.content, case .reaction = type {
return []
// return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)]
} else {
let content: MediaEditorComposerStickerEntity.Content
switch entity.content {

View File

@ -2284,7 +2284,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
if gestureRecognizer === self.dismissPanGestureRecognizer {
let location = gestureRecognizer.location(in: self.entitiesView)
if self.isDisplayingTool || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil {
if self.controller?.isEditingStory == true || self.isDisplayingTool || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil {
return false
}
return true
@ -3029,7 +3029,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let self {
self.mediaEditor?.setAudioTrack(nil)
self.requestUpdate(transition: .easeInOut(duration: 0.25))
// strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15))
}
}
)

View File

@ -1005,12 +1005,7 @@ public final class MediaToolsScreen: ViewController {
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet: Bool
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let isTablet = layout.metrics.isTablet
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0

View File

@ -89,6 +89,7 @@ swift_library(
"//submodules/TelegramUI/Components/NavigationSearchComponent",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/OptionButtonComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
],
visibility = [
"//visibility:public",

View File

@ -30,18 +30,22 @@ final class StoryItemContentComponent: Component {
let strings: PresentationStrings
let peer: EnginePeer
let item: EngineStoryItem
let availableReactions: StoryAvailableReactions?
let audioMode: StoryContentItem.AudioMode
let isVideoBuffering: Bool
let isCurrent: Bool
let activateReaction: (UIView, MessageReaction.Reaction) -> Void
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool) {
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
self.context = context
self.strings = strings
self.peer = peer
self.item = item
self.availableReactions = availableReactions
self.audioMode = audioMode
self.isVideoBuffering = isVideoBuffering
self.isCurrent = isCurrent
self.activateReaction = activateReaction
}
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool {
@ -57,6 +61,9 @@ final class StoryItemContentComponent: Component {
if lhs.item != rhs.item {
return false
}
if lhs.availableReactions != rhs.availableReactions {
return false
}
if lhs.isVideoBuffering != rhs.isVideoBuffering {
return false
}
@ -68,6 +75,7 @@ final class StoryItemContentComponent: Component {
final class View: StoryContentItem.View {
private let imageView: StoryItemImageView
private let overlaysView: StoryItemOverlaysView
private var videoNode: UniversalVideoNode?
private var loadingEffectView: StoryItemLoadingEffectView?
@ -107,12 +115,14 @@ final class StoryItemContentComponent: Component {
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
self.imageView = StoryItemImageView()
self.overlaysView = StoryItemOverlaysView()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.addSubview(self.imageView)
self.addSubview(self.overlaysView)
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
guard let self else {
@ -120,6 +130,13 @@ final class StoryItemContentComponent: Component {
}
self.updateProgressMode(update: true)
}
self.overlaysView.activate = { [weak self] view, reaction in
guard let self, let component = self.component else {
return
}
component.activateReaction(view, reaction)
}
}
required init?(coder: NSCoder) {
@ -449,6 +466,9 @@ final class StoryItemContentComponent: Component {
return result
}
}
if let result = self.overlaysView.hitTest(self.convert(point, to: self.overlaysView), with: event) {
return result
}
return nil
}
@ -579,11 +599,23 @@ final class StoryItemContentComponent: Component {
attemptSynchronous: synchronousLoad,
transition: transition
)
self.overlaysView.update(
context: component.context,
strings: component.strings,
peer: component.peer,
story: component.item,
availableReactions: component.availableReactions,
size: availableSize,
isCaptureProtected: component.item.isForwardingDisabled,
attemptSynchronous: synchronousLoad,
transition: transition
)
applyState = true
if self.imageView.isContentLoaded {
self.contentLoaded = true
}
transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.overlaysView, frame: CGRect(origin: CGPoint(), size: availableSize))
var dimensions: CGSize?
switch messageMedia {

View File

@ -0,0 +1,205 @@
import Foundation
import UIKit
import AccountContext
import TelegramCore
import Postbox
import SwiftSignalKit
import ComponentFlow
import TinyThumbnail
import ImageBlur
import MediaResources
import Display
import TelegramPresentationData
import BundleIconComponent
import MultilineTextComponent
import AppBundle
import EmojiTextAttachmentView
import TextFormat
final class StoryItemOverlaysView: UIView {
private static let coverImage: UIImage = {
return UIImage(bundleImageName: "Stories/ReactionOutline")!
}()
private final class ItemView: HighlightTrackingButton {
private let coverView: UIImageView
private var stickerView: EmojiTextAttachmentView?
private var file: TelegramMediaFile?
private var reaction: MessageReaction.Reaction?
var activate: ((UIView, MessageReaction.Reaction) -> Void)?
override init(frame: CGRect) {
self.coverView = UIImageView(image: StoryItemOverlaysView.coverImage)
super.init(frame: frame)
self.addSubview(self.coverView)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(0.9, 0.9, 1.0))
} else {
let transition: Transition = .immediate
transition.setSublayerTransform(view: self, transform: CATransform3DIdentity)
self.layer.animateSpring(from: 0.9 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4)
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let activate = self.activate, let reaction = self.reaction else {
return
}
activate(self, reaction)
}
func update(
context: AccountContext,
reaction: MessageReaction.Reaction,
availableReactions: StoryAvailableReactions?,
synchronous: Bool,
size: CGSize
) {
self.reaction = reaction
let insets = UIEdgeInsets(top: -0.08, left: -0.05, bottom: -0.01, right: -0.02)
self.coverView.frame = CGRect(origin: CGPoint(x: size.width * insets.left, y: size.height * insets.top), size: CGSize(width: size.width - size.width * insets.left - size.width * insets.right, height: size.height - size.height * insets.top - size.height * insets.bottom))
let minSide = floor(min(200.0, min(size.width, size.height)) * 0.65)
let itemSize = CGSize(width: minSide, height: minSide)
var file: TelegramMediaFile? = self.file
if self.file == nil {
switch reaction {
case .builtin:
if let availableReactions {
for reactionItem in availableReactions.reactionItems {
if reactionItem.reaction.rawValue == reaction {
file = reactionItem.stillAnimation
break
}
}
}
case let .custom(fileId):
let _ = fileId
}
}
if self.file?.fileId != file?.fileId, let file {
self.file = file
let stickerView: EmojiTextAttachmentView
if let current = self.stickerView {
stickerView = current
} else {
stickerView = EmojiTextAttachmentView(
context: context,
userLocation: .other,
emoji: ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: file.fileId.id,
file: file
),
file: file,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: UIColor(white: 0.0, alpha: 0.1),
pointSize: CGSize(width: itemSize.width, height: itemSize.height)
)
stickerView.isUserInteractionEnabled = false
self.stickerView = stickerView
self.addSubview(stickerView)
}
stickerView.frame = itemSize.centered(around: CGPoint(x: size.width * 0.5, y: size.height * 0.47))
}
}
}
private var itemViews: [Int: ItemView] = [:]
var activate: ((UIView, MessageReaction.Reaction) -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for (_, itemView) in self.itemViews {
if let result = itemView.hitTest(self.convert(point, to: itemView), with: event) {
return result
}
}
return nil
}
func update(
context: AccountContext,
strings: PresentationStrings,
peer: EnginePeer,
story: EngineStoryItem,
availableReactions: StoryAvailableReactions?,
size: CGSize,
isCaptureProtected: Bool,
attemptSynchronous: Bool,
transition: Transition
) {
var nextId = 0
for mediaArea in story.mediaAreas {
switch mediaArea {
case let .reaction(coordinates, reaction):
let referenceSize = size
let areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height)
let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height)
if targetFrame.width < 5.0 || targetFrame.height < 5.0 {
continue
}
let itemView: ItemView
let itemId = nextId
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemView = ItemView(frame: CGRect())
itemView.activate = { [weak self] view, reaction in
self?.activate?(view, reaction)
}
self.itemViews[itemId] = itemView
self.addSubview(itemView)
}
transition.setPosition(view: itemView, position: targetFrame.center)
transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: targetFrame.size))
transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(coordinates.rotation * (CGFloat.pi / 180.0), 0.0, 0.0, 1.0))
itemView.update(
context: context,
reaction: reaction,
availableReactions: availableReactions,
synchronous: attemptSynchronous,
size: targetFrame.size
)
nextId += 1
default:
break
}
}
}
}

View File

@ -920,7 +920,11 @@ public final class StoryItemSetContainerComponent: Component {
}
for area in component.slice.item.storyItem.mediaAreas {
if isPoint(point, in: area) {
if case .reaction = area {
continue
}
if isPoint(point, in: area) {
selectedMediaArea = area
break
}
@ -1483,9 +1487,16 @@ public final class StoryItemSetContainerComponent: Component {
strings: component.strings,
peer: component.slice.peer,
item: item.storyItem,
availableReactions: component.availableReactions,
audioMode: component.audioMode,
isVideoBuffering: visibleItem.isBuffering,
isCurrent: index == centralIndex
isCurrent: index == centralIndex,
activateReaction: { [weak self] reactionView, reaction in
guard let self else {
return
}
self.sendMessageContext.activateInlineReaction(view: self, reactionView: reactionView, reaction: reaction)
}
)),
environment: {
itemEnvironment

View File

@ -3268,102 +3268,7 @@ final class StoryItemSetContainerSendMessage {
}
controller?.push(locationController)
}))
case let .reaction(_, reaction):
if component.slice.peer.id != component.context.account.peerId {
let animateWithReactionItem: (ReactionItem) -> Void = { [weak self, weak view] reactionItem in
guard let self, let view else {
return
}
self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak view] in
guard let view, let component = view.component else {
return
}
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: reaction).start()
let referenceSize = view.controlsContainerView.frame.size
let size = CGSize(width: 16.0, height: mediaArea.coordinates.height / 100.0 * referenceSize.height * 1.1)
var targetFrame = CGRect(x: mediaArea.coordinates.x / 100.0 * referenceSize.width - size.width / 2.0, y: mediaArea.coordinates.y / 100.0 * referenceSize.height - size.height / 2.0, width: size.width, height: size.height)
let maxSide = min(300.0, max(targetFrame.width, targetFrame.height))
targetFrame = CGSize(width: maxSide, height: maxSide).centered(around: targetFrame.center)
//targetFrame = targetFrame.insetBy(dx: -50.0, dy: -50.0)
targetFrame = view.controlsContainerView.convert(targetFrame, to: view)
let targetView = UIView(frame: targetFrame)
targetView.isUserInteractionEnabled = false
view.addSubview(targetView)
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false)
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
view.standaloneReactionAnimation = nil
standaloneReactionAnimation.view.removeFromSuperview()
}
view.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = view.bounds
standaloneReactionAnimation.animateReactionSelection(
context: component.context,
theme: component.theme,
animationCache: component.context.animationCache,
reaction: reactionItem,
avatarPeers: [],
playHaptic: true,
isLarge: false,
hideCenterAnimation: true,
targetView: targetView,
addStandaloneReactionAnimation: { [weak view] standaloneReactionAnimation in
guard let view else {
return
}
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
view.standaloneReactionAnimation = nil
standaloneReactionAnimation.view.removeFromSuperview()
}
view.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = view.bounds
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
},
completion: { [weak targetView, weak standaloneReactionAnimation] in
targetView?.removeFromSuperview()
standaloneReactionAnimation?.view.removeFromSuperview()
}
)
})
}
switch reaction {
case .builtin:
if let availableReactions = component.availableReactions {
for reactionItem in availableReactions.reactionItems {
if reactionItem.reaction.rawValue == reaction {
animateWithReactionItem(reactionItem)
break
}
}
}
case let .custom(fileId):
let _ = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> deliverOnMainQueue).start(next: { files in
if let itemFile = files[fileId] {
let reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)),
appearAnimation: itemFile,
stillAnimation: itemFile,
listAnimation: itemFile,
largeListAnimation: itemFile,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
)
animateWithReactionItem(reactionItem)
}
})
}
}
case .reaction:
return
}
@ -3397,4 +3302,100 @@ final class StoryItemSetContainerSendMessage {
self.menuController = menuController
view.updateIsProgressPaused()
}
func activateInlineReaction(view: StoryItemSetContainerComponent.View, reactionView: UIView, reaction: MessageReaction.Reaction) {
guard let component = view.component else {
return
}
if component.slice.peer.id != component.context.account.peerId {
let animateWithReactionItem: (ReactionItem) -> Void = { [weak self, weak view] reactionItem in
guard let self, let view else {
return
}
self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak view] in
guard let view, let component = view.component else {
return
}
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: reaction).start()
let targetFrame = reactionView.convert(reactionView.bounds, to: view)
let targetView = UIView(frame: targetFrame)
targetView.isUserInteractionEnabled = false
view.addSubview(targetView)
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false)
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
view.standaloneReactionAnimation = nil
standaloneReactionAnimation.view.removeFromSuperview()
}
view.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = view.bounds
standaloneReactionAnimation.animateReactionSelection(
context: component.context,
theme: component.theme,
animationCache: component.context.animationCache,
reaction: reactionItem,
avatarPeers: [],
playHaptic: true,
isLarge: false,
hideCenterAnimation: true,
targetView: targetView,
addStandaloneReactionAnimation: { [weak view] standaloneReactionAnimation in
guard let view else {
return
}
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
view.standaloneReactionAnimation = nil
standaloneReactionAnimation.view.removeFromSuperview()
}
view.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = view.bounds
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
},
completion: { [weak targetView, weak standaloneReactionAnimation] in
targetView?.removeFromSuperview()
standaloneReactionAnimation?.view.removeFromSuperview()
}
)
})
}
switch reaction {
case .builtin:
if let availableReactions = component.availableReactions {
for reactionItem in availableReactions.reactionItems {
if reactionItem.reaction.rawValue == reaction {
animateWithReactionItem(reactionItem)
break
}
}
}
case let .custom(fileId):
let _ = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> deliverOnMainQueue).start(next: { files in
if let itemFile = files[fileId] {
let reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)),
appearAnimation: itemFile,
stillAnimation: itemFile,
listAnimation: itemFile,
largeListAnimation: itemFile,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
)
animateWithReactionItem(reactionItem)
}
})
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ReactionOutline.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -917,6 +917,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
self.sendClipboardTextEvent(requestId: requestId, fillData: fillData)
}
case "web_app_request_write_access":
self.requestWriteAccess()
case "web_app_request_phone":
self.shareAccountContact()
case "web_app_invoke_custom_method":
if let json, let requestId = json["req_id"] as? String, let method = json["method"] as? String, let params = json["params"] {
var paramsString: String?
if let string = params as? String {
paramsString = string
} else if let data1 = try? JSONSerialization.data(withJSONObject: params, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) {
paramsString = convertedString
}
self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}")
}
default:
break
}
@ -959,7 +973,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
var resultString: String?
if let string = data as? String {
resultString = string
} else if let data1 = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted), let convertedString = String(data: data1, encoding: String.Encoding.utf8) {
} else if let data1 = try? JSONSerialization.data(withJSONObject: data, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) {
resultString = convertedString
}
if let resultString = resultString {
@ -1066,6 +1080,92 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString)
}
fileprivate func requestWriteAccess() {
guard let controller = self.controller, !self.dismissed else {
return
}
let sendEvent: (Bool) -> Void = { success in
var paramsString: String
if success {
paramsString = "{status: \"allowed\"}"
} else {
paramsString = "{status: \"cancelled\"}"
}
self.webView?.sendEvent(name: "write_access_requested", data: paramsString)
}
let _ = (self.context.engine.messages.canBotSendMessages(botId: controller.botId)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, let controller = self.controller else {
return
}
if result {
sendEvent(true)
} else {
controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: "Allow Sending Messages?", text: "Allow \(controller.botName) to send messages?", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId)
|> deliverOnMainQueue).start(completed: {
sendEvent(true)
})
})]), in: .window(.root))
}
})
}
fileprivate func shareAccountContact() {
guard let controller = self.controller else {
return
}
let sendEvent: (Bool) -> Void = { success in
var paramsString: String
if success {
paramsString = "{status: \"sent\"}"
} else {
paramsString = "{status: \"cancelled\"}"
}
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
}
controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let self, let botId = self.controller?.botId, let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
let _ = enqueueMessages(account: self.context.account, peerId: botId, messages: [
.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
]).start()
sendEvent(true)
}
})
})]), in: .window(.root))
}
fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) {
guard let controller = self.controller, !self.dismissed else {
return
}
let _ = (self.context.engine.messages.invokeBotCustomMethod(botId: controller.botId, method: method, params: params)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
let paramsString = "{req_id: \"\(requestId)\", result: \(result)}"
self.webView?.sendEvent(name: "custom_method_invoked", data: paramsString)
})
}
}
fileprivate var controllerNode: Node {