mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20: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_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
|
||||
case .stroke:
|
||||
nextStyle = .regular
|
||||
case .blur:
|
||||
nextStyle = .regular
|
||||
}
|
||||
textEntity.style = nextStyle
|
||||
updateEntityView.invoke((textEntity.uuid, false))
|
||||
@ -3515,6 +3517,8 @@ public final class DrawingToolsInteraction {
|
||||
nextStyle = .regular
|
||||
case .stroke:
|
||||
nextStyle = .regular
|
||||
case .blur:
|
||||
nextStyle = .regular
|
||||
}
|
||||
textEntity.style = nextStyle
|
||||
entityView.update()
|
||||
|
@ -48,6 +48,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
|
||||
if case .file(_, .reaction) = entity.content {
|
||||
let backgroundNode = ASImageNode()
|
||||
backgroundNode.layer.zPosition = -1000.0
|
||||
backgroundNode.image = UIImage(bundleImageName: "Media Editor/ReactionBackground")
|
||||
backgroundNode.displaysAsynchronously = false
|
||||
self.addSubnode(backgroundNode)
|
||||
@ -319,15 +320,13 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
var boundingSize = CGSize(width: sideSize, height: sideSize)
|
||||
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
backgroundNode.frame = CGRect(origin: .zero, size: boundingSize)
|
||||
backgroundNode.frame = CGRect(origin: .zero, size: boundingSize).insetBy(dx: -5.0, dy: -5.0)
|
||||
boundingSize = CGSize(width: floor(sideSize * 0.63), height: floor(sideSize * 0.63))
|
||||
}
|
||||
|
||||
let imageSize = self.dimensions.aspectFitted(boundingSize)
|
||||
var imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
|
||||
imageFrame = imageFrame.offsetBy(dx: -3.0, dy: -9.0)
|
||||
}
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = imageFrame
|
||||
if let animationNode = self.animationNode {
|
||||
@ -351,6 +350,24 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
}
|
||||
}
|
||||
|
||||
private var isReaction: Bool {
|
||||
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion() {
|
||||
super.animateInsertion()
|
||||
|
||||
if self.isReaction {
|
||||
Queue.mainQueue().after(0.2) {
|
||||
let _ = self.selectedTapAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onDeselection() {
|
||||
let _ = self.dismissReactionSelection()
|
||||
}
|
||||
@ -560,10 +577,14 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size))
|
||||
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale)
|
||||
|
||||
let isReaction = self.isReaction
|
||||
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
|
||||
|
||||
|
||||
if animated {
|
||||
let isCurrentlyMirrored = ((self.imageNode.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
var isCurrentlyMirrored = ((self.imageNode.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
if isReaction {
|
||||
isCurrentlyMirrored = ((self.backgroundNode?.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
}
|
||||
var animationSourceTransform = CATransform3DIdentity
|
||||
var animationTargetTransform = CATransform3DIdentity
|
||||
if isCurrentlyMirrored {
|
||||
@ -574,23 +595,44 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
|
||||
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
|
||||
}
|
||||
self.imageNode.transform = animationSourceTransform
|
||||
self.animationNode?.transform = animationSourceTransform
|
||||
if isReaction {
|
||||
self.backgroundNode?.transform = animationSourceTransform
|
||||
|
||||
let values = [1.0, 0.01, 1.0]
|
||||
let keyTimes = [0.0, 0.5, 1.0]
|
||||
self.animationNode?.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.25, keyPath: "transform.scale.x", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
|
||||
} else {
|
||||
self.imageNode.transform = animationSourceTransform
|
||||
self.animationNode?.transform = animationSourceTransform
|
||||
self.videoNode?.transform = animationSourceTransform
|
||||
}
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.imageNode.transform = animationTargetTransform
|
||||
self.animationNode?.transform = animationTargetTransform
|
||||
self.videoNode?.transform = animationTargetTransform
|
||||
if isReaction {
|
||||
self.backgroundNode?.transform = animationTargetTransform
|
||||
} else {
|
||||
self.imageNode.transform = animationTargetTransform
|
||||
self.animationNode?.transform = animationTargetTransform
|
||||
self.videoNode?.transform = animationTargetTransform
|
||||
}
|
||||
}, completion: { finished in
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
if isReaction {
|
||||
self.backgroundNode?.transform = staticTransform
|
||||
} else {
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
}
|
||||
})
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
if isReaction {
|
||||
self.backgroundNode?.transform = staticTransform
|
||||
} else {
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
}
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
@ -607,12 +649,9 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
|
||||
selectionView.transform = .identity
|
||||
let maxSide = max(self.selectionBounds.width, self.selectionBounds.height)
|
||||
var center = self.selectionBounds.center
|
||||
let center = self.selectionBounds.center
|
||||
|
||||
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
|
||||
center = center.offsetBy(dx: -8.0 * scale, dy: -18.0 * scale)
|
||||
}
|
||||
|
||||
selectionView.center = self.convert(center, to: selectionView.superview)
|
||||
|
||||
|
@ -27,6 +27,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
return self.entity as! DrawingTextEntity
|
||||
}
|
||||
|
||||
let blurredBackgroundView: BlurredBackgroundView
|
||||
let textView: DrawingTextView
|
||||
var customEmojiContainerView: CustomEmojiContainerView?
|
||||
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
@ -35,6 +36,10 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in }
|
||||
|
||||
init(context: AccountContext, entity: DrawingTextEntity) {
|
||||
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
|
||||
self.blurredBackgroundView.clipsToBounds = true
|
||||
self.blurredBackgroundView.isHidden = true
|
||||
|
||||
self.textView = DrawingTextView(frame: .zero)
|
||||
self.textView.clipsToBounds = false
|
||||
|
||||
@ -56,6 +61,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.textView.delegate = self
|
||||
self.addSubview(self.blurredBackgroundView)
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.emojiViewProvider = { emoji in
|
||||
@ -174,6 +180,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
textColor = color
|
||||
case .stroke:
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
case .blur:
|
||||
textColor = color
|
||||
}
|
||||
|
||||
self.emojiRects = customEmojiRects
|
||||
@ -466,6 +474,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
textColor = color
|
||||
case .stroke:
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
case .blur:
|
||||
textColor = color
|
||||
}
|
||||
|
||||
guard let visualText = text.mutableCopy() as? NSMutableAttributedString else {
|
||||
@ -629,6 +639,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
||||
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
self.textView.strokeColor = color
|
||||
self.textView.frameColor = nil
|
||||
case .blur:
|
||||
break
|
||||
}
|
||||
self.textView.tintColor = self.textView.text.isEmpty ? .white : cursorColor
|
||||
|
||||
|
@ -1527,7 +1527,7 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let (layout, _) = self.currentLayout {
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
if layout.metrics.isTablet {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -1586,10 +1586,7 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
|
||||
|
||||
var effectiveExpanded = self.isExpanded
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
effectiveExpanded = true
|
||||
}
|
||||
let effectiveExpanded = self.isExpanded || layout.metrics.isTablet
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset
|
||||
|
@ -12,6 +12,7 @@ enum DrawingTextStyle: Equatable {
|
||||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
case blur
|
||||
|
||||
init(style: DrawingTextEntity.Style) {
|
||||
switch style {
|
||||
@ -23,6 +24,8 @@ enum DrawingTextStyle: Equatable {
|
||||
self = .semi
|
||||
case .stroke:
|
||||
self = .stroke
|
||||
case .blur:
|
||||
self = .blur
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -502,6 +505,8 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
styleImage = state.image(.semi)
|
||||
case .stroke:
|
||||
styleImage = state.image(.stroke)
|
||||
case .blur:
|
||||
styleImage = state.image(.stroke)
|
||||
}
|
||||
|
||||
var fontAvailableWidth: CGFloat = context.availableSize.width
|
||||
|
@ -455,11 +455,11 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
}
|
||||
|
||||
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|
||||
|> then(
|
||||
assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|
||||
|> delay(0.03, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false)
|
||||
// |> then(
|
||||
// assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|
||||
// |> delay(0.03, queue: Queue.concurrentDefaultQueue())
|
||||
// )
|
||||
|
||||
if stories {
|
||||
self.imageNode.contentUpdated = { [weak self] image in
|
||||
|
@ -676,68 +676,70 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
if !controller.didSetupGroups {
|
||||
controller.didSetupGroups = true
|
||||
controller.groupsPromise.set(
|
||||
combineLatest(
|
||||
self.mediaAssetsContext.fetchAssetsCollections(.album),
|
||||
self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
|
||||
)
|
||||
|> map { albums, smartAlbums -> [MediaGroupItem] in
|
||||
var collections: [PHAssetCollection] = []
|
||||
smartAlbums.enumerateObjects { collection, _, _ in
|
||||
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
|
||||
collections.append(collection)
|
||||
Queue.concurrentDefaultQueue().after(0.3) {
|
||||
controller.groupsPromise.set(
|
||||
combineLatest(
|
||||
self.mediaAssetsContext.fetchAssetsCollections(.album),
|
||||
self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
|
||||
)
|
||||
|> map { albums, smartAlbums -> [MediaGroupItem] in
|
||||
var collections: [PHAssetCollection] = []
|
||||
smartAlbums.enumerateObjects { collection, _, _ in
|
||||
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
|
||||
collections.append(collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
smartAlbums.enumerateObjects { collection, index, _ in
|
||||
var supportedAlbums: [PHAssetCollectionSubtype] = [
|
||||
.smartAlbumBursts,
|
||||
.smartAlbumPanoramas,
|
||||
.smartAlbumScreenshots,
|
||||
.smartAlbumSelfPortraits,
|
||||
.smartAlbumSlomoVideos,
|
||||
.smartAlbumTimelapses,
|
||||
.smartAlbumVideos,
|
||||
.smartAlbumAllHidden
|
||||
]
|
||||
if #available(iOS 11, *) {
|
||||
supportedAlbums.append(.smartAlbumAnimated)
|
||||
supportedAlbums.append(.smartAlbumDepthEffect)
|
||||
supportedAlbums.append(.smartAlbumLivePhotos)
|
||||
smartAlbums.enumerateObjects { collection, index, _ in
|
||||
var supportedAlbums: [PHAssetCollectionSubtype] = [
|
||||
.smartAlbumBursts,
|
||||
.smartAlbumPanoramas,
|
||||
.smartAlbumScreenshots,
|
||||
.smartAlbumSelfPortraits,
|
||||
.smartAlbumSlomoVideos,
|
||||
.smartAlbumTimelapses,
|
||||
.smartAlbumVideos,
|
||||
.smartAlbumAllHidden
|
||||
]
|
||||
if #available(iOS 11, *) {
|
||||
supportedAlbums.append(.smartAlbumAnimated)
|
||||
supportedAlbums.append(.smartAlbumDepthEffect)
|
||||
supportedAlbums.append(.smartAlbumLivePhotos)
|
||||
}
|
||||
if supportedAlbums.contains(collection.assetCollectionSubtype) {
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
if result.count > 0 {
|
||||
collections.append(collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
if supportedAlbums.contains(collection.assetCollectionSubtype) {
|
||||
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
if result.count > 0 {
|
||||
collections.append(collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
if result.count > 0 {
|
||||
collections.append(collection)
|
||||
}
|
||||
}
|
||||
|
||||
var items: [MediaGroupItem] = []
|
||||
for collection in collections {
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
let firstItem: PHAsset?
|
||||
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
|
||||
firstItem = result.lastObject
|
||||
} else {
|
||||
firstItem = result.firstObject
|
||||
}
|
||||
items.append(
|
||||
MediaGroupItem(
|
||||
collection: collection,
|
||||
firstItem: firstItem,
|
||||
count: result.count
|
||||
|
||||
var items: [MediaGroupItem] = []
|
||||
for collection in collections {
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
let firstItem: PHAsset?
|
||||
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
|
||||
firstItem = result.lastObject
|
||||
} else {
|
||||
firstItem = result.firstObject
|
||||
}
|
||||
items.append(
|
||||
MediaGroupItem(
|
||||
collection: collection,
|
||||
firstItem: firstItem,
|
||||
count: result.count
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
return items
|
||||
}
|
||||
return items
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if case .notDetermined = mediaAccess, !self.requestedMediaAccess {
|
||||
self.requestedMediaAccess = true
|
||||
|
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)
|
||||
case let .messageActionCustomAction(message):
|
||||
return TelegramMediaAction(action: .customText(text: message, entities: []))
|
||||
case let .messageActionBotAllowed(_, domain, _):
|
||||
return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain ?? ""))
|
||||
case let .messageActionBotAllowed(flags, domain, app):
|
||||
if let domain = domain {
|
||||
return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain))
|
||||
} else if case let .botApp(_, _, _, _, appName, _, _, _, _) = app {
|
||||
var type: BotSendMessageAccessGrantedType?
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
type = .request
|
||||
} else if (flags & (1 << 1)) != 0 {
|
||||
type = .attachMenu
|
||||
}
|
||||
return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case .messageActionSecureValuesSentMe:
|
||||
return nil
|
||||
case let .messageActionSecureValuesSent(types):
|
||||
|
@ -23,6 +23,11 @@ public enum SentSecureValueType: Int32 {
|
||||
case temporaryRegistration = 12
|
||||
}
|
||||
|
||||
public enum BotSendMessageAccessGrantedType: Int32 {
|
||||
case attachMenu = 0
|
||||
case request = 1
|
||||
}
|
||||
|
||||
public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
public enum ForumTopicEditComponent: PostboxCoding, Equatable {
|
||||
case title(String)
|
||||
@ -86,6 +91,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool)
|
||||
case customText(text: String, entities: [MessageTextEntity])
|
||||
case botDomainAccessGranted(domain: String)
|
||||
case botAppAccessGranted(appName: String, type: BotSendMessageAccessGrantedType?)
|
||||
case botSentSecureValues(types: [SentSecureValueType])
|
||||
case peerJoined
|
||||
case phoneNumberRequest
|
||||
@ -195,6 +201,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
} else {
|
||||
self = .unknown
|
||||
}
|
||||
case 35:
|
||||
self = .botAppAccessGranted(appName: decoder.decodeStringForKey("app", orElse: ""), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
|
||||
default:
|
||||
self = .unknown
|
||||
}
|
||||
@ -362,6 +370,14 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
|
||||
case let .setSameChatWallpaper(wallpaper):
|
||||
encoder.encodeInt32(34, forKey: "_rawValue")
|
||||
encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper")
|
||||
case let .botAppAccessGranted(appName, type):
|
||||
encoder.encodeInt32(35, forKey: "_rawValue")
|
||||
encoder.encodeString(appName, forKey: "app")
|
||||
if let type = type {
|
||||
encoder.encodeInt32(type.rawValue, forKey: "atp")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "atp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,3 +236,72 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage
|
||||
|> castError(RequestAppWebViewError.self)
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
func _internal_canBotSendMessages(postbox: Postbox, network: Network, botId: PeerId) -> Signal<Bool, NoError> {
|
||||
return postbox.transaction { transaction -> Signal<Bool, NoError> in
|
||||
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
return network.request(Api.functions.bots.canSendMessage(bot: inputUser))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> map { result -> Bool in
|
||||
if case .boolTrue = result {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
func _internal_allowBotSendMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, botId: PeerId) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
|
||||
return .never()
|
||||
}
|
||||
|
||||
return network.request(Api.functions.bots.allowSendMessage(bot: inputUser))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { updates -> Api.Updates? in
|
||||
if let updates = updates {
|
||||
stateManager.addUpdates(updates)
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
public enum InvokeBotCustomMethodError {
|
||||
case generic
|
||||
}
|
||||
|
||||
func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId: PeerId, method: String, params: String) -> Signal<String, InvokeBotCustomMethodError> {
|
||||
let params = Api.DataJSON.dataJSON(data: params)
|
||||
return postbox.transaction { transaction -> Signal<String, InvokeBotCustomMethodError> in
|
||||
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return network.request(Api.functions.bots.invokeWebViewCustomMethod(bot: inputUser, customMethod: method, params: params))
|
||||
|> mapError { _ -> InvokeBotCustomMethodError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> String in
|
||||
if case let .dataJSON(data) = result {
|
||||
return data
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|> castError(InvokeBotCustomMethodError.self)
|
||||
|> switchToLatest
|
||||
}
|
||||
|
@ -507,6 +507,18 @@ public extension TelegramEngine {
|
||||
public func sendWebViewData(botId: PeerId, buttonText: String, data: String) -> Signal<Never, SendWebViewDataError> {
|
||||
return _internal_sendWebViewData(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId, buttonText: buttonText, data: data)
|
||||
}
|
||||
|
||||
public func canBotSendMessages(botId: PeerId) -> Signal<Bool, NoError> {
|
||||
return _internal_canBotSendMessages(postbox: self.account.postbox, network: self.account.network, botId: botId)
|
||||
}
|
||||
|
||||
public func allowBotSendMessages(botId: PeerId) -> Signal<Never, NoError> {
|
||||
return _internal_allowBotSendMessages(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId)
|
||||
}
|
||||
|
||||
public func invokeBotCustomMethod(botId: PeerId, method: String, params: String) -> Signal<String, InvokeBotCustomMethodError> {
|
||||
return _internal_invokeBotCustomMethod(postbox: self.account.postbox, network: self.account.network, botId: botId, method: method, params: params)
|
||||
}
|
||||
|
||||
public func addBotToAttachMenu(botId: PeerId, allowWrite: Bool) -> Signal<Bool, AddBotToAttachMenuError> {
|
||||
return _internal_addBotToAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, allowWrite: allowWrite)
|
||||
|
@ -629,6 +629,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())
|
||||
case let .botDomainAccessGranted(domain):
|
||||
attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor)
|
||||
case let .botAppAccessGranted(appName, _):
|
||||
attributedString = NSAttributedString(string: strings.AuthSessions_MessageApp(appName).string, font: titleFont, textColor: primaryTextColor)
|
||||
case let .botSentSecureValues(types):
|
||||
var typesString = ""
|
||||
var hasIdentity = false
|
||||
|
@ -1757,7 +1757,7 @@ public class CameraScreen: ViewController {
|
||||
self.backgroundView.alpha = 1.0
|
||||
})
|
||||
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
self.controller?.statusBar.updateStatusBarStyle(.Hide, animated: true)
|
||||
}
|
||||
|
||||
@ -2075,12 +2075,7 @@ public class CameraScreen: ViewController {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
isTablet = true
|
||||
} else {
|
||||
isTablet = false
|
||||
}
|
||||
let isTablet = layout.metrics.isTablet
|
||||
|
||||
var topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
|
||||
let previewSize: CGSize
|
||||
@ -2615,7 +2610,7 @@ public class CameraScreen: ViewController {
|
||||
self.node.camera?.stopCapture(invalidate: true)
|
||||
self.isDismissed = true
|
||||
if animated {
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
self.node.animateOut(completion: {
|
||||
self.dismiss(animated: false)
|
||||
@ -2637,7 +2632,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
return
|
||||
}
|
||||
|
||||
@ -2683,7 +2678,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
return
|
||||
}
|
||||
if dismissing {
|
||||
|
@ -80,7 +80,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
|
||||
public var referenceDrawingSize: CGSize
|
||||
public var position: CGPoint
|
||||
public var scale: CGFloat
|
||||
public var scale: CGFloat {
|
||||
didSet {
|
||||
if case let .file(_, type) = self.content, case .reaction = type {
|
||||
self.scale = max(0.75, min(2.0, self.scale))
|
||||
}
|
||||
}
|
||||
}
|
||||
public var rotation: CGFloat
|
||||
public var mirrored: Bool
|
||||
|
||||
|
@ -61,6 +61,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
case blur
|
||||
}
|
||||
|
||||
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] {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
if case let .file(_, type) = entity.content, case .reaction = type, let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
|
||||
return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)]
|
||||
if case let .file(_, type) = entity.content, case .reaction = type {
|
||||
return []
|
||||
// return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)]
|
||||
} else {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
|
@ -2284,7 +2284,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
if gestureRecognizer === self.dismissPanGestureRecognizer {
|
||||
let location = gestureRecognizer.location(in: self.entitiesView)
|
||||
if self.isDisplayingTool || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil {
|
||||
if self.controller?.isEditingStory == true || self.isDisplayingTool || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -3029,7 +3029,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let self {
|
||||
self.mediaEditor?.setAudioTrack(nil)
|
||||
self.requestUpdate(transition: .easeInOut(duration: 0.25))
|
||||
// strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1005,12 +1005,7 @@ public final class MediaToolsScreen: ViewController {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
isTablet = true
|
||||
} else {
|
||||
isTablet = false
|
||||
}
|
||||
let isTablet = layout.metrics.isTablet
|
||||
|
||||
let previewSize: CGSize
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
|
||||
|
@ -89,6 +89,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/NavigationSearchComponent",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/OptionButtonComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -30,18 +30,22 @@ final class StoryItemContentComponent: Component {
|
||||
let strings: PresentationStrings
|
||||
let peer: EnginePeer
|
||||
let item: EngineStoryItem
|
||||
let availableReactions: StoryAvailableReactions?
|
||||
let audioMode: StoryContentItem.AudioMode
|
||||
let isVideoBuffering: Bool
|
||||
let isCurrent: Bool
|
||||
let activateReaction: (UIView, MessageReaction.Reaction) -> Void
|
||||
|
||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool) {
|
||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
|
||||
self.context = context
|
||||
self.strings = strings
|
||||
self.peer = peer
|
||||
self.item = item
|
||||
self.availableReactions = availableReactions
|
||||
self.audioMode = audioMode
|
||||
self.isVideoBuffering = isVideoBuffering
|
||||
self.isCurrent = isCurrent
|
||||
self.activateReaction = activateReaction
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool {
|
||||
@ -57,6 +61,9 @@ final class StoryItemContentComponent: Component {
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
if lhs.availableReactions != rhs.availableReactions {
|
||||
return false
|
||||
}
|
||||
if lhs.isVideoBuffering != rhs.isVideoBuffering {
|
||||
return false
|
||||
}
|
||||
@ -68,6 +75,7 @@ final class StoryItemContentComponent: Component {
|
||||
|
||||
final class View: StoryContentItem.View {
|
||||
private let imageView: StoryItemImageView
|
||||
private let overlaysView: StoryItemOverlaysView
|
||||
private var videoNode: UniversalVideoNode?
|
||||
private var loadingEffectView: StoryItemLoadingEffectView?
|
||||
|
||||
@ -107,12 +115,14 @@ final class StoryItemContentComponent: Component {
|
||||
override init(frame: CGRect) {
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
self.imageView = StoryItemImageView()
|
||||
self.overlaysView = StoryItemOverlaysView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
|
||||
self.addSubview(self.imageView)
|
||||
self.addSubview(self.overlaysView)
|
||||
|
||||
self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in
|
||||
guard let self else {
|
||||
@ -120,6 +130,13 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
self.updateProgressMode(update: true)
|
||||
}
|
||||
|
||||
self.overlaysView.activate = { [weak self] view, reaction in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.activateReaction(view, reaction)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -449,6 +466,9 @@ final class StoryItemContentComponent: Component {
|
||||
return result
|
||||
}
|
||||
}
|
||||
if let result = self.overlaysView.hitTest(self.convert(point, to: self.overlaysView), with: event) {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -579,11 +599,23 @@ final class StoryItemContentComponent: Component {
|
||||
attemptSynchronous: synchronousLoad,
|
||||
transition: transition
|
||||
)
|
||||
self.overlaysView.update(
|
||||
context: component.context,
|
||||
strings: component.strings,
|
||||
peer: component.peer,
|
||||
story: component.item,
|
||||
availableReactions: component.availableReactions,
|
||||
size: availableSize,
|
||||
isCaptureProtected: component.item.isForwardingDisabled,
|
||||
attemptSynchronous: synchronousLoad,
|
||||
transition: transition
|
||||
)
|
||||
applyState = true
|
||||
if self.imageView.isContentLoaded {
|
||||
self.contentLoaded = true
|
||||
}
|
||||
transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
transition.setFrame(view: self.overlaysView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
var dimensions: CGSize?
|
||||
switch messageMedia {
|
||||
|
@ -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 {
|
||||
if isPoint(point, in: area) {
|
||||
if case .reaction = area {
|
||||
continue
|
||||
}
|
||||
|
||||
if isPoint(point, in: area) {
|
||||
selectedMediaArea = area
|
||||
break
|
||||
}
|
||||
@ -1483,9 +1487,16 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
strings: component.strings,
|
||||
peer: component.slice.peer,
|
||||
item: item.storyItem,
|
||||
availableReactions: component.availableReactions,
|
||||
audioMode: component.audioMode,
|
||||
isVideoBuffering: visibleItem.isBuffering,
|
||||
isCurrent: index == centralIndex
|
||||
isCurrent: index == centralIndex,
|
||||
activateReaction: { [weak self] reactionView, reaction in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendMessageContext.activateInlineReaction(view: self, reactionView: reactionView, reaction: reaction)
|
||||
}
|
||||
)),
|
||||
environment: {
|
||||
itemEnvironment
|
||||
|
@ -3268,102 +3268,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
}
|
||||
controller?.push(locationController)
|
||||
}))
|
||||
case let .reaction(_, reaction):
|
||||
if component.slice.peer.id != component.context.account.peerId {
|
||||
let animateWithReactionItem: (ReactionItem) -> Void = { [weak self, weak view] reactionItem in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
|
||||
self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak view] in
|
||||
guard let view, let component = view.component else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: reaction).start()
|
||||
|
||||
let referenceSize = view.controlsContainerView.frame.size
|
||||
let size = CGSize(width: 16.0, height: mediaArea.coordinates.height / 100.0 * referenceSize.height * 1.1)
|
||||
var targetFrame = CGRect(x: mediaArea.coordinates.x / 100.0 * referenceSize.width - size.width / 2.0, y: mediaArea.coordinates.y / 100.0 * referenceSize.height - size.height / 2.0, width: size.width, height: size.height)
|
||||
let maxSide = min(300.0, max(targetFrame.width, targetFrame.height))
|
||||
targetFrame = CGSize(width: maxSide, height: maxSide).centered(around: targetFrame.center)
|
||||
//targetFrame = targetFrame.insetBy(dx: -50.0, dy: -50.0)
|
||||
targetFrame = view.controlsContainerView.convert(targetFrame, to: view)
|
||||
|
||||
let targetView = UIView(frame: targetFrame)
|
||||
targetView.isUserInteractionEnabled = false
|
||||
view.addSubview(targetView)
|
||||
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false)
|
||||
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
||||
|
||||
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
|
||||
view.standaloneReactionAnimation = nil
|
||||
standaloneReactionAnimation.view.removeFromSuperview()
|
||||
}
|
||||
view.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
|
||||
standaloneReactionAnimation.frame = view.bounds
|
||||
standaloneReactionAnimation.animateReactionSelection(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
animationCache: component.context.animationCache,
|
||||
reaction: reactionItem,
|
||||
avatarPeers: [],
|
||||
playHaptic: true,
|
||||
isLarge: false,
|
||||
hideCenterAnimation: true,
|
||||
targetView: targetView,
|
||||
addStandaloneReactionAnimation: { [weak view] standaloneReactionAnimation in
|
||||
guard let view else {
|
||||
return
|
||||
}
|
||||
|
||||
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
|
||||
view.standaloneReactionAnimation = nil
|
||||
standaloneReactionAnimation.view.removeFromSuperview()
|
||||
}
|
||||
view.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
|
||||
standaloneReactionAnimation.frame = view.bounds
|
||||
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
||||
},
|
||||
completion: { [weak targetView, weak standaloneReactionAnimation] in
|
||||
targetView?.removeFromSuperview()
|
||||
standaloneReactionAnimation?.view.removeFromSuperview()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
switch reaction {
|
||||
case .builtin:
|
||||
if let availableReactions = component.availableReactions {
|
||||
for reactionItem in availableReactions.reactionItems {
|
||||
if reactionItem.reaction.rawValue == reaction {
|
||||
animateWithReactionItem(reactionItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .custom(fileId):
|
||||
let _ = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|
||||
|> deliverOnMainQueue).start(next: { files in
|
||||
if let itemFile = files[fileId] {
|
||||
let reactionItem = ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)),
|
||||
appearAnimation: itemFile,
|
||||
stillAnimation: itemFile,
|
||||
listAnimation: itemFile,
|
||||
largeListAnimation: itemFile,
|
||||
applicationAnimation: nil,
|
||||
largeApplicationAnimation: nil,
|
||||
isCustom: true
|
||||
)
|
||||
animateWithReactionItem(reactionItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
case .reaction:
|
||||
return
|
||||
}
|
||||
|
||||
@ -3397,4 +3302,100 @@ final class StoryItemSetContainerSendMessage {
|
||||
self.menuController = menuController
|
||||
view.updateIsProgressPaused()
|
||||
}
|
||||
|
||||
func activateInlineReaction(view: StoryItemSetContainerComponent.View, reactionView: UIView, reaction: MessageReaction.Reaction) {
|
||||
guard let component = view.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if component.slice.peer.id != component.context.account.peerId {
|
||||
let animateWithReactionItem: (ReactionItem) -> Void = { [weak self, weak view] reactionItem in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
|
||||
self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak view] in
|
||||
guard let view, let component = view.component else {
|
||||
return
|
||||
}
|
||||
let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, reaction: reaction).start()
|
||||
|
||||
let targetFrame = reactionView.convert(reactionView.bounds, to: view)
|
||||
|
||||
let targetView = UIView(frame: targetFrame)
|
||||
targetView.isUserInteractionEnabled = false
|
||||
view.addSubview(targetView)
|
||||
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false)
|
||||
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
||||
|
||||
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
|
||||
view.standaloneReactionAnimation = nil
|
||||
standaloneReactionAnimation.view.removeFromSuperview()
|
||||
}
|
||||
view.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
|
||||
standaloneReactionAnimation.frame = view.bounds
|
||||
standaloneReactionAnimation.animateReactionSelection(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
animationCache: component.context.animationCache,
|
||||
reaction: reactionItem,
|
||||
avatarPeers: [],
|
||||
playHaptic: true,
|
||||
isLarge: false,
|
||||
hideCenterAnimation: true,
|
||||
targetView: targetView,
|
||||
addStandaloneReactionAnimation: { [weak view] standaloneReactionAnimation in
|
||||
guard let view else {
|
||||
return
|
||||
}
|
||||
|
||||
if let standaloneReactionAnimation = view.standaloneReactionAnimation {
|
||||
view.standaloneReactionAnimation = nil
|
||||
standaloneReactionAnimation.view.removeFromSuperview()
|
||||
}
|
||||
view.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
|
||||
standaloneReactionAnimation.frame = view.bounds
|
||||
view.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
||||
},
|
||||
completion: { [weak targetView, weak standaloneReactionAnimation] in
|
||||
targetView?.removeFromSuperview()
|
||||
standaloneReactionAnimation?.view.removeFromSuperview()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
switch reaction {
|
||||
case .builtin:
|
||||
if let availableReactions = component.availableReactions {
|
||||
for reactionItem in availableReactions.reactionItems {
|
||||
if reactionItem.reaction.rawValue == reaction {
|
||||
animateWithReactionItem(reactionItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .custom(fileId):
|
||||
let _ = (component.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|
||||
|> deliverOnMainQueue).start(next: { files in
|
||||
if let itemFile = files[fileId] {
|
||||
let reactionItem = ReactionItem(
|
||||
reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)),
|
||||
appearAnimation: itemFile,
|
||||
stillAnimation: itemFile,
|
||||
listAnimation: itemFile,
|
||||
largeListAnimation: itemFile,
|
||||
applicationAnimation: nil,
|
||||
largeApplicationAnimation: nil,
|
||||
isCustom: true
|
||||
)
|
||||
animateWithReactionItem(reactionItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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)
|
||||
}
|
||||
case "web_app_request_write_access":
|
||||
self.requestWriteAccess()
|
||||
case "web_app_request_phone":
|
||||
self.shareAccountContact()
|
||||
case "web_app_invoke_custom_method":
|
||||
if let json, let requestId = json["req_id"] as? String, let method = json["method"] as? String, let params = json["params"] {
|
||||
var paramsString: String?
|
||||
if let string = params as? String {
|
||||
paramsString = string
|
||||
} else if let data1 = try? JSONSerialization.data(withJSONObject: params, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) {
|
||||
paramsString = convertedString
|
||||
}
|
||||
self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}")
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -959,7 +973,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
var resultString: String?
|
||||
if let string = data as? String {
|
||||
resultString = string
|
||||
} else if let data1 = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted), let convertedString = String(data: data1, encoding: String.Encoding.utf8) {
|
||||
} else if let data1 = try? JSONSerialization.data(withJSONObject: data, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) {
|
||||
resultString = convertedString
|
||||
}
|
||||
if let resultString = resultString {
|
||||
@ -1066,6 +1080,92 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString)
|
||||
}
|
||||
|
||||
fileprivate func requestWriteAccess() {
|
||||
guard let controller = self.controller, !self.dismissed else {
|
||||
return
|
||||
}
|
||||
|
||||
let sendEvent: (Bool) -> Void = { success in
|
||||
var paramsString: String
|
||||
if success {
|
||||
paramsString = "{status: \"allowed\"}"
|
||||
} else {
|
||||
paramsString = "{status: \"cancelled\"}"
|
||||
}
|
||||
self.webView?.sendEvent(name: "write_access_requested", data: paramsString)
|
||||
}
|
||||
|
||||
let _ = (self.context.engine.messages.canBotSendMessages(botId: controller.botId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
sendEvent(true)
|
||||
} else {
|
||||
controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: "Allow Sending Messages?", text: "Allow \(controller.botName) to send messages?", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
|
||||
sendEvent(false)
|
||||
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
sendEvent(true)
|
||||
})
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func shareAccountContact() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let sendEvent: (Bool) -> Void = { success in
|
||||
var paramsString: String
|
||||
if success {
|
||||
paramsString = "{status: \"sent\"}"
|
||||
} else {
|
||||
paramsString = "{status: \"cancelled\"}"
|
||||
}
|
||||
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
|
||||
}
|
||||
|
||||
controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
|
||||
sendEvent(false)
|
||||
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
if let self, let botId = self.controller?.botId, let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
|
||||
let _ = enqueueMessages(account: self.context.account, peerId: botId, messages: [
|
||||
.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
|
||||
]).start()
|
||||
sendEvent(true)
|
||||
}
|
||||
})
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
|
||||
fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) {
|
||||
guard let controller = self.controller, !self.dismissed else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.messages.invokeBotCustomMethod(botId: controller.botId, method: method, params: params)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let paramsString = "{req_id: \"\(requestId)\", result: \(result)}"
|
||||
self.webView?.sendEvent(name: "custom_method_invoked", data: paramsString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var controllerNode: Node {
|
||||
|
Loading…
x
Reference in New Issue
Block a user