mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 11:23:48 +00:00
Merge commit '863c4afb910d4b23c50f802fccbcdd3ffcef7846' into experimental-3
# Conflicts: # submodules/TelegramApi/Sources/Api31.swift
This commit is contained in:
commit
c0d2dc07e5
@ -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 %@.";
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
9163
submodules/TelegramApi/Sources/Api31.swift
Normal file
9163
submodules/TelegramApi/Sources/Api31.swift
Normal file
File diff suppressed because it is too large
Load Diff
@ -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):
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
15
submodules/TelegramUI/Images.xcassets/Stories/ReactionOutline.imageset/Contents.json
vendored
Normal file
15
submodules/TelegramUI/Images.xcassets/Stories/ReactionOutline.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "ReactionOutline.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
BIN
submodules/TelegramUI/Images.xcassets/Stories/ReactionOutline.imageset/ReactionOutline.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Stories/ReactionOutline.imageset/ReactionOutline.pdf
vendored
Normal file
Binary file not shown.
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user