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_1" = "1 Viewer";
"Story.ViewList.ViewerCount_any" = "%d Viewers"; "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 nextStyle = .regular
case .stroke: case .stroke:
nextStyle = .regular nextStyle = .regular
case .blur:
nextStyle = .regular
} }
textEntity.style = nextStyle textEntity.style = nextStyle
updateEntityView.invoke((textEntity.uuid, false)) updateEntityView.invoke((textEntity.uuid, false))
@ -3515,6 +3517,8 @@ public final class DrawingToolsInteraction {
nextStyle = .regular nextStyle = .regular
case .stroke: case .stroke:
nextStyle = .regular nextStyle = .regular
case .blur:
nextStyle = .regular
} }
textEntity.style = nextStyle textEntity.style = nextStyle
entityView.update() entityView.update()

View File

@ -48,6 +48,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
if case .file(_, .reaction) = entity.content { if case .file(_, .reaction) = entity.content {
let backgroundNode = ASImageNode() let backgroundNode = ASImageNode()
backgroundNode.layer.zPosition = -1000.0
backgroundNode.image = UIImage(bundleImageName: "Media Editor/ReactionBackground") backgroundNode.image = UIImage(bundleImageName: "Media Editor/ReactionBackground")
backgroundNode.displaysAsynchronously = false backgroundNode.displaysAsynchronously = false
self.addSubnode(backgroundNode) self.addSubnode(backgroundNode)
@ -319,15 +320,13 @@ public final class DrawingStickerEntityView: DrawingEntityView {
var boundingSize = CGSize(width: sideSize, height: sideSize) var boundingSize = CGSize(width: sideSize, height: sideSize)
if let backgroundNode = self.backgroundNode { 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)) boundingSize = CGSize(width: floor(sideSize * 0.63), height: floor(sideSize * 0.63))
} }
let imageSize = self.dimensions.aspectFitted(boundingSize) 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) let 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)
}
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = imageFrame self.imageNode.frame = imageFrame
if let animationNode = self.animationNode { 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() { func onDeselection() {
let _ = self.dismissReactionSelection() let _ = self.dismissReactionSelection()
} }
@ -560,10 +577,14 @@ public final class DrawingStickerEntityView: DrawingEntityView {
self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size)) self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size))
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale) 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) let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
if animated { 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 animationSourceTransform = CATransform3DIdentity
var animationTargetTransform = CATransform3DIdentity var animationTargetTransform = CATransform3DIdentity
if isCurrentlyMirrored { if isCurrentlyMirrored {
@ -574,23 +595,44 @@ public final class DrawingStickerEntityView: DrawingEntityView {
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0) animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
} }
self.imageNode.transform = animationSourceTransform if isReaction {
self.animationNode?.transform = animationSourceTransform 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: { UIView.animate(withDuration: 0.25, animations: {
self.imageNode.transform = animationTargetTransform if isReaction {
self.animationNode?.transform = animationTargetTransform self.backgroundNode?.transform = animationTargetTransform
self.videoNode?.transform = animationTargetTransform } else {
self.imageNode.transform = animationTargetTransform
self.animationNode?.transform = animationTargetTransform
self.videoNode?.transform = animationTargetTransform
}
}, completion: { finished in }, completion: { finished in
self.imageNode.transform = staticTransform if isReaction {
self.animationNode?.transform = staticTransform self.backgroundNode?.transform = staticTransform
self.videoNode?.transform = staticTransform } else {
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoNode?.transform = staticTransform
}
}) })
} else { } else {
CATransaction.begin() CATransaction.begin()
CATransaction.setDisableActions(true) CATransaction.setDisableActions(true)
self.imageNode.transform = staticTransform if isReaction {
self.animationNode?.transform = staticTransform self.backgroundNode?.transform = staticTransform
self.videoNode?.transform = staticTransform } else {
self.imageNode.transform = staticTransform
self.animationNode?.transform = staticTransform
self.videoNode?.transform = staticTransform
}
CATransaction.commit() CATransaction.commit()
} }
@ -607,12 +649,9 @@ public final class DrawingStickerEntityView: DrawingEntityView {
selectionView.transform = .identity selectionView.transform = .identity
let maxSide = max(self.selectionBounds.width, self.selectionBounds.height) 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 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) 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 return self.entity as! DrawingTextEntity
} }
let blurredBackgroundView: BlurredBackgroundView
let textView: DrawingTextView let textView: DrawingTextView
var customEmojiContainerView: CustomEmojiContainerView? var customEmojiContainerView: CustomEmojiContainerView?
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
@ -35,6 +36,10 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in } var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in }
init(context: AccountContext, entity: DrawingTextEntity) { 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 = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false self.textView.clipsToBounds = false
@ -56,6 +61,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
super.init(context: context, entity: entity) super.init(context: context, entity: entity)
self.textView.delegate = self self.textView.delegate = self
self.addSubview(self.blurredBackgroundView)
self.addSubview(self.textView) self.addSubview(self.textView)
self.emojiViewProvider = { emoji in self.emojiViewProvider = { emoji in
@ -174,6 +180,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
textColor = color textColor = color
case .stroke: case .stroke:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
case .blur:
textColor = color
} }
self.emojiRects = customEmojiRects self.emojiRects = customEmojiRects
@ -466,6 +474,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
textColor = color textColor = color
case .stroke: case .stroke:
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
case .blur:
textColor = color
} }
guard let visualText = text.mutableCopy() as? NSMutableAttributedString else { 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.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
self.textView.strokeColor = color self.textView.strokeColor = color
self.textView.frameColor = nil self.textView.frameColor = nil
case .blur:
break
} }
self.textView.tintColor = self.textView.text.isEmpty ? .white : cursorColor 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 { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let (layout, _) = self.currentLayout { if let (layout, _) = self.currentLayout {
if case .regular = layout.metrics.widthClass { if layout.metrics.isTablet {
return false 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)) 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 let effectiveExpanded = self.isExpanded || layout.metrics.isTablet
if case .regular = layout.metrics.widthClass {
effectiveExpanded = true
}
let isLandscape = layout.orientation == .landscape let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset

View File

@ -12,6 +12,7 @@ enum DrawingTextStyle: Equatable {
case filled case filled
case semi case semi
case stroke case stroke
case blur
init(style: DrawingTextEntity.Style) { init(style: DrawingTextEntity.Style) {
switch style { switch style {
@ -23,6 +24,8 @@ enum DrawingTextStyle: Equatable {
self = .semi self = .semi
case .stroke: case .stroke:
self = .stroke self = .stroke
case .blur:
self = .blur
} }
} }
} }
@ -502,6 +505,8 @@ final class TextSettingsComponent: CombinedComponent {
styleImage = state.image(.semi) styleImage = state.image(.semi)
case .stroke: case .stroke:
styleImage = state.image(.stroke) styleImage = state.image(.stroke)
case .blur:
styleImage = state.image(.stroke)
} }
var fontAvailableWidth: CGFloat = context.availableSize.width 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) 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) let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false)
|> then( // |> then(
assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false) // assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|> delay(0.03, queue: Queue.concurrentDefaultQueue()) // |> delay(0.03, queue: Queue.concurrentDefaultQueue())
) // )
if stories { if stories {
self.imageNode.contentUpdated = { [weak self] image in self.imageNode.contentUpdated = { [weak self] image in

View File

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

View File

@ -23,6 +23,11 @@ public enum SentSecureValueType: Int32 {
case temporaryRegistration = 12 case temporaryRegistration = 12
} }
public enum BotSendMessageAccessGrantedType: Int32 {
case attachMenu = 0
case request = 1
}
public enum TelegramMediaActionType: PostboxCoding, Equatable { public enum TelegramMediaActionType: PostboxCoding, Equatable {
public enum ForumTopicEditComponent: PostboxCoding, Equatable { public enum ForumTopicEditComponent: PostboxCoding, Equatable {
case title(String) 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 paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool)
case customText(text: String, entities: [MessageTextEntity]) case customText(text: String, entities: [MessageTextEntity])
case botDomainAccessGranted(domain: String) case botDomainAccessGranted(domain: String)
case botAppAccessGranted(appName: String, type: BotSendMessageAccessGrantedType?)
case botSentSecureValues(types: [SentSecureValueType]) case botSentSecureValues(types: [SentSecureValueType])
case peerJoined case peerJoined
case phoneNumberRequest case phoneNumberRequest
@ -195,6 +201,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else { } else {
self = .unknown self = .unknown
} }
case 35:
self = .botAppAccessGranted(appName: decoder.decodeStringForKey("app", orElse: ""), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
default: default:
self = .unknown self = .unknown
} }
@ -362,6 +370,14 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .setSameChatWallpaper(wallpaper): case let .setSameChatWallpaper(wallpaper):
encoder.encodeInt32(34, forKey: "_rawValue") encoder.encodeInt32(34, forKey: "_rawValue")
encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper") 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) |> castError(RequestAppWebViewError.self)
|> switchToLatest |> 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> { 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) 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> { 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) 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()) 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): case let .botDomainAccessGranted(domain):
attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor) 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): case let .botSentSecureValues(types):
var typesString = "" var typesString = ""
var hasIdentity = false var hasIdentity = false

View File

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

View File

@ -80,7 +80,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
public var referenceDrawingSize: CGSize public var referenceDrawingSize: CGSize
public var position: CGPoint 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 rotation: CGFloat
public var mirrored: Bool public var mirrored: Bool

View File

@ -61,6 +61,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
case filled case filled
case semi case semi
case stroke case stroke
case blur
} }
public enum Animation: Codable, Equatable { 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] { func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] {
if let entity = entity as? DrawingStickerEntity { 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]) { if case let .file(_, type) = entity.content, case .reaction = type {
return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)] return []
// return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)]
} else { } else {
let content: MediaEditorComposerStickerEntity.Content let content: MediaEditorComposerStickerEntity.Content
switch entity.content { switch entity.content {

View File

@ -2284,7 +2284,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
if gestureRecognizer === self.dismissPanGestureRecognizer { if gestureRecognizer === self.dismissPanGestureRecognizer {
let location = gestureRecognizer.location(in: self.entitiesView) 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 false
} }
return true return true
@ -3029,7 +3029,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let self { if let self {
self.mediaEditor?.setAudioTrack(nil) self.mediaEditor?.setAudioTrack(nil)
self.requestUpdate(transition: .easeInOut(duration: 0.25)) 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 let isFirstTime = self.validLayout == nil
self.validLayout = layout self.validLayout = layout
let isTablet: Bool let isTablet = layout.metrics.isTablet
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let previewSize: CGSize let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0

View File

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

View File

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

View File

@ -3268,102 +3268,7 @@ final class StoryItemSetContainerSendMessage {
} }
controller?.push(locationController) controller?.push(locationController)
})) }))
case let .reaction(_, reaction): case .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)
}
})
}
}
return return
} }
@ -3397,4 +3302,100 @@ final class StoryItemSetContainerSendMessage {
self.menuController = menuController self.menuController = menuController
view.updateIsProgressPaused() 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) 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: default:
break break
} }
@ -959,7 +973,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
var resultString: String? var resultString: String?
if let string = data as? String { if let string = data as? String {
resultString = 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 resultString = convertedString
} }
if let resultString = resultString { if let resultString = resultString {
@ -1066,6 +1080,92 @@ public final class WebAppController: ViewController, AttachmentContainable {
} }
self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString) 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 { fileprivate var controllerNode: Node {