Merge commit '21121e1924cb6133965447631a3193ec4dbeb31d'

# Conflicts:
#	Telegram/Telegram-iOS/en.lproj/Localizable.strings
This commit is contained in:
Ali 2022-12-13 22:21:00 +04:00
commit c140900b33
47 changed files with 2177 additions and 485 deletions

View File

@ -8482,3 +8482,15 @@ Sorry for the inconvenience.";
"CacheEvictionMenu.CategoryExceptions_1" = "%@ Exception";
"CacheEvictionMenu.CategoryExceptions_any" = "%@ Exceptions";
"Conversation.Messages_1" = "%@ message";
"Conversation.Messages_any" = "%@ messages";
"Notification.SuggestedProfilePhoto" = "Suggested Profile Photo";
"Notification.SuggestedProfileVideo" = "Suggested Profile Video";
"PhotoEditor.SetProfilePhoto" = "Set Profile Photo";
"PhotoEditor.SetProfileVideo" = "Set Profile Video";
"PhotoEditor.SetAsMyPhoto" = "Set as My Photo";
"PhotoEditor.SetAsMyVideo" = "Set as My Video";

View File

@ -47,6 +47,12 @@ public final class AnimationNode : ASDisplayNode {
self.colorCallbacks.append(colorCallback)
view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/
}
if let value = colors["__allcolors__"] {
for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
}
return view
@ -75,6 +81,12 @@ public final class AnimationNode : ASDisplayNode {
self.colorCallbacks.append(colorCallback)
view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/
}
if let value = colors["__allcolors__"] {
for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
}
return view

View File

@ -573,6 +573,17 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
}
}
} else if case let .action(action) = self.media, case let .suggestedProfilePhoto(image) = action.action, let image = image {
isRound = true
self.playIcon.isHidden = true
if let largest = largestImageRepresentation(image.representations) {
dimensions = largest.dimensions.cgSize
if !self.requestedImage {
self.requestedImage = true
let signal = mediaGridMessagePhoto(account: self.context.account, photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads)
self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads)
}
}
} else if case let .file(file) = self.media {
if file.isInstantVideo {
isRound = true
@ -1294,9 +1305,25 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
guard let strongSelf = self else {
return
}
let cachedPeerData = peerView.cachedData
if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo {
if let photo = maybePhoto, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
let cachedPeerData = peerView.cachedData as? CachedUserData
var personalPhoto: TelegramMediaImage?
var profilePhoto: TelegramMediaImage?
var isKnown = false
if let cachedPeerData = cachedPeerData {
if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto {
personalPhoto = maybePersonalPhoto
isKnown = true
}
if case let .known(maybePhoto) = cachedPeerData.photo {
profilePhoto = maybePhoto
isKnown = true
}
}
if isKnown {
let photo = personalPhoto ?? profilePhoto
if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
@ -1936,6 +1963,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
break inner
}
} else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image {
let fitSize = contentImageSize
contentImageSpecs.append((message, .action(action), fitSize))
}
}
}
@ -3165,7 +3195,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: floor((measureLayout.size.height - contentImageSize.height) / 2.0))
var validMediaIds: [EngineMedia.Id] = []
for (message, media, mediaSize) in contentImageSpecs {
guard let mediaId = media.id else {
var mediaId = media.id
if mediaId == nil, case let .action(action) = media, case let .suggestedProfilePhoto(image) = action.action {
mediaId = image?.id
}
guard let mediaId = mediaId else {
continue
}
validMediaIds.append(mediaId)

View File

@ -24,6 +24,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/UndoUI:UndoUI",
"//submodules/AnimationUI:AnimationUI",
],
visibility = [
"//visibility:public",

View File

@ -102,6 +102,7 @@ public final class ContextMenuActionItem {
public let badge: ContextMenuActionBadge?
public let icon: (PresentationTheme) -> UIImage?
public let iconSource: ContextMenuActionItemIconSource?
public let animationName: String?
public let textIcon: (PresentationTheme) -> UIImage?
public let textLinkAction: () -> Void
public let action: ((Action) -> Void)?
@ -116,6 +117,7 @@ public final class ContextMenuActionItem {
badge: ContextMenuActionBadge? = nil,
icon: @escaping (PresentationTheme) -> UIImage?,
iconSource: ContextMenuActionItemIconSource? = nil,
animationName: String? = nil,
textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil },
textLinkAction: @escaping () -> Void = {},
action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?
@ -130,6 +132,7 @@ public final class ContextMenuActionItem {
badge: badge,
icon: icon,
iconSource: iconSource,
animationName: animationName,
textIcon: textIcon,
textLinkAction: textLinkAction,
action: action.flatMap { action in
@ -150,6 +153,7 @@ public final class ContextMenuActionItem {
badge: ContextMenuActionBadge? = nil,
icon: @escaping (PresentationTheme) -> UIImage?,
iconSource: ContextMenuActionItemIconSource? = nil,
animationName: String? = nil,
textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil },
textLinkAction: @escaping () -> Void = {},
action: ((Action) -> Void)?
@ -163,6 +167,7 @@ public final class ContextMenuActionItem {
self.badge = badge
self.icon = icon
self.iconSource = iconSource
self.animationName = animationName
self.textIcon = textIcon
self.textLinkAction = textLinkAction
self.action = action

View File

@ -12,6 +12,7 @@ import Markdown
import EntityKeyboard
import AnimationCache
import MultiAnimationRenderer
import AnimationUI
public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
var wantsFullWidth: Bool { get }
@ -63,6 +64,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
private let titleLabelNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
private var iconDisposable: Disposable?
@ -94,7 +96,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
self.iconNode = ASImageNode()
self.iconNode.isAccessibilityElement = false
self.iconNode.isUserInteractionEnabled = false
super.init()
self.isAccessibilityElement = true
@ -275,6 +277,14 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
}
} else if let image = self.iconNode.image {
iconSize = image.size
} else if let animationName = self.item.animationName {
if self.animationNode == nil {
let animationNode = AnimationNode(animation: animationName, colors: ["__allcolors__": titleColor], scale: 1.0)
animationNode.loop()
self.addSubnode(animationNode)
self.animationNode = animationNode
}
iconSize = CGSize(width: 24.0, height: 24.0)
} else {
let iconImage = self.item.icon(presentationData.theme)
self.iconNode.image = iconImage
@ -323,6 +333,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
let iconWidth = max(standardIconWidth, iconSize.width)
let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
transition.updateFrame(node: self.iconNode, frame: iconFrame, beginWithCurrentState: true)
if let animationNode = self.animationNode {
transition.updateFrame(node: animationNode, frame: iconFrame, beginWithCurrentState: true)
}
}
})
}

View File

@ -284,7 +284,7 @@ class DrawingGesturePipeline {
}
}
var mode: Mode = .polyline {
var mode: Mode = .location {
didSet {
if [.location, .polyline].contains(self.mode) {
self.gestureRecognizer?.usePredictedTouches = false

View File

@ -279,7 +279,7 @@ struct DrawingState: Equatable {
return DrawingState(
selectedTool: .pen,
tools: [
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xffffff), size: 0.3, mode: .round)),
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xe22400), size: 0.25, mode: .round)),
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.5, mode: .round)),
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.5, mode: .round)),
.pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.5, mode: .round)),
@ -466,6 +466,8 @@ private final class DrawingScreenComponent: CombinedComponent {
self.drawingState = .initial
self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false)
self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff)
self.updateToolState.invoke(self.drawingState.currentToolState)
}
private var currentToolState: DrawingToolState {

View File

@ -71,7 +71,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
var tool: Tool = .pen
var toolColor: DrawingColor = DrawingColor(color: .white)
var toolBrushSize: CGFloat = 0.35
var toolBrushSize: CGFloat = 0.25
var toolHasArrow: Bool = false
var stateUpdated: (NavigationState) -> Void = { _ in }
@ -577,7 +577,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
func updateToolState(_ state: DrawingToolState) {
switch state {
case let .pen(brushState):
self.drawingGesturePipeline?.mode = .polyline
self.drawingGesturePipeline?.mode = .location
self.tool = .pen
self.toolColor = brushState.color
self.toolBrushSize = brushState.size

File diff suppressed because it is too large Load Diff

View File

@ -114,10 +114,17 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
galleryMedia = fullMedia
} else if let action = media as? TelegramMediaAction {
switch action.action {
case let .photoUpdated(image):
case let .photoUpdated(image), let .suggestedProfilePhoto(image):
if let peer = messageMainPeer(EngineMessage(message)), let image = image {
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action")])
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: .roundRect(15.5), remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in
let sourceCorners: AvatarGalleryController.SourceCorners
if case .photoUpdated = action.action {
sourceCorners = .roundRect(15.5)
} else {
sourceCorners = .round
}
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in
})
return .chatAvatars(galleryController, image)

View File

@ -15,7 +15,7 @@ func createEmitterBehavior(type: String) -> NSObject {
return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
}
private func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? {
func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? {
var size = originalSize
var position = position
var scale: CGFloat = 1.0
@ -58,8 +58,7 @@ public class InvisibleInkDustNode: ASDisplayNode {
private let emitterMaskFillNode: ASDisplayNode
public var isRevealed = false
private var exploding = false
private var isExploding = false
public init(textNode: TextNode?) {
self.textNode = textNode
@ -158,8 +157,8 @@ public class InvisibleInkDustNode: ASDisplayNode {
transition.updateAlpha(node: self, alpha: 1.0)
transition.updateAlpha(node: textNode, alpha: 0.0)
if self.exploding {
self.exploding = false
if self.isExploding {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
}
}
@ -171,7 +170,7 @@ public class InvisibleInkDustNode: ASDisplayNode {
}
self.isRevealed = true
self.exploding = true
self.isExploding = true
let position = gestureRecognizer.location(in: self.view)
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
@ -227,66 +226,13 @@ public class InvisibleInkDustNode: ASDisplayNode {
}
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.exploding = false
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.textSpotNode.layer.removeAllAnimations()
self.emitterSpotNode.layer.removeAllAnimations()
self.emitterMaskFillNode.layer.removeAllAnimations()
}
var spoilersLength: Int = 0
if let spoilers = textNode.cachedLayout?.spoilers {
for spoiler in spoilers {
spoilersLength += spoiler.0.length
}
}
let timeToRead = min(45.0, ceil(max(4.0, Double(spoilersLength) * 0.04)))
Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) {
if let (_, color, _, _, _) = self.currentParams {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let animation = POPBasicAnimation()
animation.property = (POPAnimatableProperty.property(withName: "color", initializer: { property in
property?.readBlock = { node, values in
if let color = (node as! InvisibleInkDustNode).emitter?.color {
if let a = color.components {
values?[0] = a[0]
values?[1] = a[1]
values?[2] = a[2]
values?[3] = a[3]
}
}
}
property?.writeBlock = { node, values in
if let values = values, let color = CGColor(colorSpace: colorSpace, components: values) {
(node as! InvisibleInkDustNode).animColor = color
(node as! InvisibleInkDustNode).updateEmitter()
}
}
property?.threshold = 0.4
}) as! POPAnimatableProperty)
animation.fromValue = self.emitter?.color
animation.toValue = color
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.1
animation.completionBlock = { [weak self] _, _ in
if let strongSelf = self {
strongSelf.animColor = nil
strongSelf.updateEmitter()
}
}
self.pop_add(animation, forKey: "color")
}
Queue.mainQueue().after(0.15) {
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
transition.updateAlpha(node: self, alpha: 1.0)
transition.updateAlpha(node: textNode, alpha: 0.0, completion: { [weak self] _ in
self?.isRevealed = false
})
}
}
}
private func updateEmitter() {

View File

@ -13,15 +13,36 @@ public class MediaDustNode: ASDisplayNode {
private var emitterNode: ASDisplayNode
private var emitter: CAEmitterCell?
private var emitterLayer: CAEmitterLayer?
private let emitterMaskNode: ASDisplayNode
private let emitterSpotNode: ASImageNode
private let emitterMaskFillNode: ASDisplayNode
public var isRevealed = false
private var isExploding = false
public var revealed: () -> Void = {}
public override init() {
self.emitterNode = ASDisplayNode()
self.emitterNode.isUserInteractionEnabled = false
self.emitterNode.clipsToBounds = true
self.emitterMaskNode = ASDisplayNode()
self.emitterSpotNode = ASImageNode()
self.emitterSpotNode.contentMode = .scaleToFill
self.emitterSpotNode.isUserInteractionEnabled = false
self.emitterMaskFillNode = ASDisplayNode()
self.emitterMaskFillNode.backgroundColor = .white
self.emitterMaskFillNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.emitterNode)
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
}
public override func didLoad() {
@ -51,8 +72,25 @@ public class MediaDustNode: ASDisplayNode {
scaleBehavior.setValue("scale", forKey: "keyPath")
scaleBehavior.setValue([0.0, 0.5], forKey: "values")
scaleBehavior.setValue([0.0, 0.05], forKey: "locations")
let behaviors = [alphaBehavior, scaleBehavior]
let randomAttractor0 = createEmitterBehavior(type: "simpleAttractor")
randomAttractor0.setValue("randomAttractor0", forKey: "name")
randomAttractor0.setValue(20, forKey: "falloff")
randomAttractor0.setValue(35, forKey: "radius")
randomAttractor0.setValue(5, forKey: "stiffness")
randomAttractor0.setValue(NSValue(cgPoint: .zero), forKey: "position")
let randomAttractor1 = createEmitterBehavior(type: "simpleAttractor")
randomAttractor1.setValue("randomAttractor1", forKey: "name")
randomAttractor1.setValue(20, forKey: "falloff")
randomAttractor1.setValue(35, forKey: "radius")
randomAttractor1.setValue(5, forKey: "stiffness")
randomAttractor1.setValue(NSValue(cgPoint: .zero), forKey: "position")
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let behaviors = [randomAttractor0, randomAttractor1, fingerAttractor, alphaBehavior, scaleBehavior]
let emitterLayer = CAEmitterLayer()
emitterLayer.masksToBounds = true
@ -62,14 +100,143 @@ public class MediaDustNode: ASDisplayNode {
emitterLayer.seed = arc4random()
emitterLayer.emitterShape = .rectangle
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
emitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.fingerAttractor.stiffness")
emitterLayer.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterLayer = emitterLayer
self.emitterNode.layer.addSublayer(emitterLayer)
self.updateEmitter()
self.setupRandomAnimations()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
}
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
guard !self.isRevealed else {
return
}
self.isRevealed = true
self.isExploding = true
let position = gestureRecognizer.location(in: self.view)
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position")
let maskSize = self.emitterNode.frame.size
Queue.concurrentDefaultQueue().async {
let emitterMaskImage = generateMaskImage(size: maskSize, position: position, inverse: true)
Queue.mainQueue().async {
self.emitterSpotNode.image = emitterMaskImage
}
}
Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) {
let xFactor = (position.x / self.emitterNode.frame.width - 0.5) * 2.0
let yFactor = (position.y / self.emitterNode.frame.height - 0.5) * 2.0
let maxFactor = max(abs(xFactor), abs(yFactor))
let scaleAddition = maxFactor * 4.0
let durationAddition = -maxFactor * 0.2
self.supernode?.view.mask = self.emitterMaskNode.view
self.emitterSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0)
self.emitterSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height)
self.emitterSpotNode.position = position
self.emitterSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.45 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in
self?.revealed()
self?.alpha = 0.0
self?.supernode?.view.mask = nil
})
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterSpotNode.layer.removeAllAnimations()
self.emitterMaskFillNode.layer.removeAllAnimations()
}
}
private var didSetupAnimations = false
private func setupRandomAnimations() {
guard self.frame.width > 0.0, self.emitterLayer != nil, !self.didSetupAnimations else {
return
}
self.didSetupAnimations = true
let falloffAnimation1 = CABasicAnimation(keyPath: "emitterBehaviors.randomAttractor0.falloff")
falloffAnimation1.beginTime = 0.0
falloffAnimation1.fillMode = .both
falloffAnimation1.isRemovedOnCompletion = false
falloffAnimation1.autoreverses = true
falloffAnimation1.repeatCount = .infinity
falloffAnimation1.duration = 2.0
falloffAnimation1.fromValue = -20.0 as NSNumber
falloffAnimation1.toValue = 60.0 as NSNumber
falloffAnimation1.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.emitterLayer?.add(falloffAnimation1, forKey: "emitterBehaviors.randomAttractor0.falloff")
let positionAnimation1 = CAKeyframeAnimation(keyPath: "emitterBehaviors.randomAttractor0.position")
positionAnimation1.beginTime = 0.0
positionAnimation1.fillMode = .both
positionAnimation1.isRemovedOnCompletion = false
positionAnimation1.autoreverses = true
positionAnimation1.repeatCount = .infinity
positionAnimation1.duration = 3.0
positionAnimation1.calculationMode = .discrete
let xInset1: CGFloat = self.frame.width * 0.2
let yInset1: CGFloat = self.frame.height * 0.2
var positionValues1: [CGPoint] = []
for _ in 0 ..< 35 {
positionValues1.append(CGPoint(x: CGFloat.random(in: xInset1 ..< self.frame.width - xInset1), y: CGFloat.random(in: yInset1 ..< self.frame.height - yInset1)))
}
positionAnimation1.values = positionValues1
self.emitterLayer?.add(positionAnimation1, forKey: "emitterBehaviors.randomAttractor0.position")
let falloffAnimation2 = CABasicAnimation(keyPath: "emitterBehaviors.randomAttractor1.falloff")
falloffAnimation2.beginTime = 0.0
falloffAnimation2.fillMode = .both
falloffAnimation2.isRemovedOnCompletion = false
falloffAnimation2.autoreverses = true
falloffAnimation2.repeatCount = .infinity
falloffAnimation2.duration = 2.0
falloffAnimation2.fromValue = -20.0 as NSNumber
falloffAnimation2.toValue = 60.0 as NSNumber
falloffAnimation2.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.emitterLayer?.add(falloffAnimation2, forKey: "emitterBehaviors.randomAttractor1.falloff")
let positionAnimation2 = CAKeyframeAnimation(keyPath: "emitterBehaviors.randomAttractor1.position")
positionAnimation2.beginTime = 0.0
positionAnimation2.fillMode = .both
positionAnimation2.isRemovedOnCompletion = false
positionAnimation2.autoreverses = true
positionAnimation2.repeatCount = .infinity
positionAnimation2.duration = 3.0
positionAnimation2.calculationMode = .discrete
let xInset2: CGFloat = self.frame.width * 0.1
let yInset2: CGFloat = self.frame.height * 0.1
var positionValues2: [CGPoint] = []
for _ in 0 ..< 35 {
positionValues2.append(CGPoint(x: CGFloat.random(in: xInset2 ..< self.frame.width - xInset2), y: CGFloat.random(in: yInset2 ..< self.frame.height - yInset2)))
}
positionAnimation2.values = positionValues2
self.emitterLayer?.add(positionAnimation2, forKey: "emitterBehaviors.randomAttractor1.position")
}
private func updateEmitter() {
guard let (size, _) = self.currentParams else {
return
@ -79,19 +246,37 @@ public class MediaDustNode: ASDisplayNode {
self.emitterLayer?.emitterSize = size
self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
let radius = max(size.width, size.height)
self.emitterLayer?.setValue(max(size.width, size.height), forKeyPath: "emitterBehaviors.fingerAttractor.radius")
self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff")
let square = Float(size.width * size.height)
Queue.mainQueue().async {
self.emitter?.birthRate = min(100000.0, square * 0.016)
self.emitter?.birthRate = min(100000.0, square * 0.02)
}
}
public func update(size: CGSize, color: UIColor) {
public func update(size: CGSize, color: UIColor, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, color)
self.emitterNode.frame = CGRect(origin: CGPoint(), size: size)
let bounds = CGRect(origin: .zero, size: size)
transition.updateFrame(node: self.emitterNode, frame: bounds)
self.emitterMaskNode.frame = bounds
self.emitterMaskFillNode.frame = bounds
if self.isNodeLoaded {
self.updateEmitter()
self.setupRandomAnimations()
}
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if !self.isRevealed {
return super.point(inside: point, with: event)
} else {
return false
}
}
}

View File

@ -245,7 +245,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
insets.bottom = 0.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0)

View File

@ -86,6 +86,12 @@
- (void)setTimer:(NSNumber *)timer forItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)timersUpdatedSignal;
- (bool)spoilerForItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)spoilerSignalForItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)spoilerSignalForIdentifier:(NSString *)identifier;
- (void)setSpoiler:(bool)spoiler forItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)spoilersUpdatedSignal;
- (UIImage *)paintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
- (UIImage *)stillPaintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)image forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;

View File

@ -21,7 +21,8 @@ typedef enum {
TGPhotoEditorControllerFromCameraIntent = (1 << 2),
TGPhotoEditorControllerWebIntent = (1 << 3),
TGPhotoEditorControllerVideoIntent = (1 << 4),
TGPhotoEditorControllerForumAvatarIntent = (1 << 5)
TGPhotoEditorControllerForumAvatarIntent = (1 << 5),
TGPhotoEditorControllerSuggestedAvatarIntent = (1 << 6)
} TGPhotoEditorControllerIntent;
@interface TGPhotoEditorController : TGOverlayController

View File

@ -11,6 +11,11 @@
@end
@protocol TGPhotoSolidRoundedButtonView <NSObject>
- (void)updateWidth:(CGFloat)width;
@end
@protocol TGPhotoPaintStickerRenderView <NSObject>
@ -127,6 +132,8 @@
@property (nonatomic, copy) id<TGCaptionPanelView>(^captionPanelView)(void);
- (UIView<TGPhotoSolidRoundedButtonView> *)solidRoundedButton:(NSString *)title action:(void(^)(void))action;
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size;
@end

View File

@ -66,6 +66,7 @@ typedef enum
- (void)setEditButtonsDisabled:(TGPhotoEditorTab)buttons;
- (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated;
- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated;
@property (nonatomic, readonly) TGPhotoEditorTab currentTabs;
- (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated;

View File

@ -2,7 +2,7 @@
@interface TGPhotoVideoEditor : NSObject
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;

View File

@ -957,6 +957,8 @@
}
}
bool spoiler = [editingContext spoilerForItem:item];
switch (asset.type)
{
case TGMediaAssetPhotoType:
@ -1029,6 +1031,10 @@
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
if (spoiler) {
dict[@"spoiler"] = @true;
}
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
return generatedItem;
}];
@ -1105,6 +1111,10 @@
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
if (spoiler) {
dict[@"spoiler"] = @true;
}
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
return generatedItem;
}];
@ -1188,6 +1198,10 @@
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
if (spoiler) {
dict[@"spoiler"] = @true;
}
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
return generatedItem;
}] catch:^SSignal *(__unused id error)
@ -1228,6 +1242,10 @@
if (groupedId != nil)
dict[@"groupedId"] = groupedId;
if (spoiler) {
dict[@"spoiler"] = @true;
}
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
return generatedItem;
}]];
@ -1297,6 +1315,10 @@
else if (groupedId != nil && !hasAnyTimers)
dict[@"groupedId"] = groupedId;
if (spoiler) {
dict[@"spoiler"] = @true;
}
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
return generatedItem;
}]];
@ -1374,6 +1396,10 @@
if (timer != nil)
dict[@"timer"] = timer;
if (spoiler) {
dict[@"spoiler"] = @true;
}
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
return generatedItem;
}]];
@ -1387,8 +1413,7 @@
break;
}
if (groupedId != nil && i == 10)
{
if (groupedId != nil && i == 10) {
i = 0;
groupedId = @([self generateGroupedId]);
}

View File

@ -54,6 +54,16 @@
@end
@interface TGMediaSpoilerUpdate : NSObject
@property (nonatomic, readonly, strong) id<TGMediaEditableItem> item;
@property (nonatomic, readonly) bool spoiler;
+ (instancetype)spoilerUpdateWithItem:(id<TGMediaEditableItem>)item spoiler:(bool)spoiler;
+ (instancetype)spoilerUpdate:(bool)spoiler;
@end
@interface TGModernCache (Private)
@ -69,6 +79,8 @@
NSMutableDictionary *_adjustments;
NSMutableDictionary *_timers;
NSNumber *_timer;
NSMutableDictionary *_spoilers;
SQueue *_queue;
@ -99,6 +111,7 @@
SPipe *_adjustmentsPipe;
SPipe *_captionPipe;
SPipe *_timerPipe;
SPipe *_spoilerPipe;
SPipe *_fullSizePipe;
SPipe *_cropPipe;
@ -119,6 +132,7 @@
_captions = [[NSMutableDictionary alloc] init];
_adjustments = [[NSMutableDictionary alloc] init];
_timers = [[NSMutableDictionary alloc] init];
_spoilers = [[NSMutableDictionary alloc] init];
_imageCache = [[TGMemoryImageCache alloc] initWithSoftMemoryLimit:[[self class] imageSoftMemoryLimit]
hardMemoryLimit:[[self class] imageHardMemoryLimit]];
@ -165,6 +179,7 @@
_adjustmentsPipe = [[SPipe alloc] init];
_captionPipe = [[SPipe alloc] init];
_timerPipe = [[SPipe alloc] init];
_spoilerPipe = [[SPipe alloc] init];
_fullSizePipe = [[SPipe alloc] init];
_cropPipe = [[SPipe alloc] init];
}
@ -596,6 +611,73 @@
#pragma mark -
- (bool)spoilerForItem:(NSObject<TGMediaEditableItem> *)item
{
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
if (itemId == nil)
return nil;
return [self _spoilerForItemId:itemId];
}
- (bool)_spoilerForItemId:(NSString *)itemId
{
if (itemId == nil)
return nil;
return _spoilers[itemId];
}
- (void)setSpoiler:(bool)spoiler forItem:(NSObject<TGMediaEditableItem> *)item
{
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
if (itemId == nil)
return;
if (spoiler)
_spoilers[itemId] = @true;
else
[_spoilers removeObjectForKey:itemId];
_spoilerPipe.sink([TGMediaSpoilerUpdate spoilerUpdateWithItem:item spoiler:spoiler]);
}
- (SSignal *)spoilerSignalForItem:(NSObject<TGMediaEditableItem> *)item
{
SSignal *updateSignal = [[_spoilerPipe.signalProducer() filter:^bool(TGMediaSpoilerUpdate *update)
{
return [update.item.uniqueIdentifier isEqualToString:item.uniqueIdentifier];
}] map:^NSNumber *(TGMediaSpoilerUpdate *update)
{
return @(update.spoiler);
}];
return [[SSignal single:@([self spoilerForItem:item])] then:updateSignal];
}
- (SSignal *)spoilerSignalForIdentifier:(NSString *)identifier
{
SSignal *updateSignal = [[_spoilerPipe.signalProducer() filter:^bool(TGMediaSpoilerUpdate *update)
{
return [update.item.uniqueIdentifier isEqualToString:identifier];
}] map:^NSNumber *(TGMediaSpoilerUpdate *update)
{
return @(update.spoiler);
}];
return [[SSignal single:@([self _spoilerForItemId:identifier])] then:updateSignal];
}
- (SSignal *)spoilersUpdatedSignal
{
return [_spoilerPipe.signalProducer() map:^id(__unused id value)
{
return @true;
}];
}
#pragma mark -
- (void)setImage:(UIImage *)image thumbnailImage:(UIImage *)thumbnailImage forItem:(id<TGMediaEditableItem>)item synchronous:(bool)synchronous
{
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
@ -1082,3 +1164,23 @@
}
@end
@implementation TGMediaSpoilerUpdate
+ (instancetype)spoilerUpdateWithItem:(id<TGMediaEditableItem>)item spoiler:(bool)spoiler
{
TGMediaSpoilerUpdate *update = [[TGMediaSpoilerUpdate alloc] init];
update->_item = item;
update->_spoiler = spoiler;
return update;
}
+ (instancetype)spoilerUpdate:(bool)spoiler
{
TGMediaSpoilerUpdate *update = [[TGMediaSpoilerUpdate alloc] init];
update->_spoiler = spoiler;
return update;
}
@end

View File

@ -13,6 +13,9 @@
@property (nonatomic, assign) bool skipTransitionIn;
@property (nonatomic, assign) bool fromCamera;
@property (nonatomic, copy) void (^cancelPressed)(void);
@property (nonatomic, copy) void (^donePressed)(void);
@property (nonatomic, copy) void (^croppingChanged)(void);
@property (nonatomic, copy) void (^togglePlayback)(void);
@ -23,7 +26,9 @@
@property (nonatomic, weak) TGPhotoEntitiesContainerView *fullEntitiesView;
@property (nonatomic, weak) TGMediaPickerGalleryVideoScrubber *scrubberView;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum;
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion;
- (void)setImage:(UIImage *)image;
- (void)setSnapshotImage:(UIImage *)snapshotImage;

View File

@ -18,6 +18,8 @@
#import "TGModernGalleryVideoView.h"
#import "TGPhotoEntitiesContainerView.h"
#import "TGPhotoPaintStickersContext.h"
#import "TGPhotoPaintController.h"
const CGFloat TGPhotoAvatarPreviewPanelSize = 96.0f;
@ -34,7 +36,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
UIView *_wrapperView;
__weak TGPhotoAvatarCropView *_cropView;
UIView *_portraitToolsWrapperView;
UIView *_landscapeToolsWrapperView;
UIView *_portraitWrapperBackgroundView;
@ -44,11 +46,16 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
UIView *_landscapeToolControlView;
UILabel *_coverLabel;
TGModernButton *_cancelButton;
UILabel *_titleLabel;
UIView<TGPhotoSolidRoundedButtonView> *_doneButton;
bool _wasPlayingBeforeCropping;
bool _scheduledTransitionIn;
bool _isForum;
bool _isSuggestion;
}
@property (nonatomic, weak) PGPhotoEditor *photoEditor;
@ -58,13 +65,14 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
@implementation TGPhotoAvatarPreviewController
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum {
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion {
self = [super initWithContext:context];
if (self != nil)
{
self.photoEditor = photoEditor;
self.previewView = previewView;
_isForum = isForum;
_isSuggestion = isSuggestion;
}
return self;
}
@ -179,6 +187,42 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
[_wrapperView addSubview:_dotImageView];
}
if (_isSuggestion) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.backgroundColor = [UIColor clearColor];
_titleLabel.font = TGBoldSystemFontOfSize(17.0f);
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.text = self.item.isVideo ? TGLocalized(@"PhotoEditor.SetProfileVideo") : TGLocalized(@"PhotoEditor.SetProfilePhoto");
[_titleLabel sizeToFit];
[_wrapperView addSubview:_titleLabel];
if (!self.item.isVideo) {
_cancelButton = [[TGModernButton alloc] init];
[_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal];
_cancelButton.titleLabel.font = TGSystemFontOfSize(17.0);
[_cancelButton sizeToFit];
[_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[_wrapperView addSubview:_cancelButton];
if (_stickersContext != nil) {
_doneButton = [_stickersContext solidRoundedButton:self.item.isVideo ? TGLocalized(@"PhotoEditor.SetAsMyVideo") : TGLocalized(@"PhotoEditor.SetAsMyPhoto") action:^{
__strong TGPhotoAvatarPreviewController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (strongSelf.donePressed != nil)
strongSelf.donePressed();
}];
[_wrapperView addSubview:_doneButton];
}
}
}
}
- (void)cancelButtonPressed {
if (self.cancelPressed != nil)
self.cancelPressed();
}
- (void)viewWillAppear:(BOOL)animated
@ -325,8 +369,16 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
[_cropView animateTransitionIn];
_cancelButton.alpha = 0.0;
_titleLabel.alpha = 0.0;
_doneButton.alpha = 0.0;
[UIView animateWithDuration:0.3f animations:^
{
_cancelButton.alpha = 1.0;
_titleLabel.alpha = 1.0;
_doneButton.alpha = 1.0;
_portraitToolsWrapperView.alpha = 1.0f;
_landscapeToolsWrapperView.alpha = 1.0f;
_dotImageView.alpha = 1.0f;
@ -499,6 +551,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_landscapeToolsWrapperView.alpha = 0.0f;
_dotImageView.alpha = 0.0f;
_dotMarkerView.alpha = 0.0f;
_cancelButton.alpha = 0.0;
_titleLabel.alpha = 0.0;
_doneButton.alpha = 0.0;
} completion:^(__unused BOOL finished)
{
if (!switching) {
@ -614,6 +669,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_portraitToolsWrapperView.alpha = 0.0f;
_landscapeToolsWrapperView.alpha = 0.0f;
_dotImageView.alpha = 0.0f;
_titleLabel.alpha = 0.0f;
_cancelButton.alpha = 0.0f;
_doneButton.alpha = 0.0;
} completion:nil];
}
@ -742,6 +800,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
screenEdges.left += safeAreaInset.left;
screenEdges.bottom -= safeAreaInset.bottom;
screenEdges.right -= safeAreaInset.right;
CGSize buttonSize = CGSizeMake(MIN(referenceSize.width, referenceSize.height) - 16.0 * 2.0, 50.0f);
[_doneButton updateWidth:buttonSize.width];
switch (orientation)
{
@ -757,6 +818,12 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height);
_cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
_doneButton.frame = CGRectMake(floor((_wrapperView.frame.size.width - buttonSize.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, buttonSize.width, buttonSize.height);
}
break;
@ -768,11 +835,16 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
}];
_landscapeToolsWrapperView.frame = CGRectMake(screenEdges.right - panelToolbarLandscapeSize, screenEdges.top, panelToolbarLandscapeSize, referenceSize.height);
_portraitToolsWrapperView.frame = CGRectMake(screenEdges.top, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height);
_cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
_doneButton.frame = CGRectMake(floor((_wrapperView.frame.size.width - buttonSize.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, buttonSize.width, buttonSize.height);
}
break;
@ -793,6 +865,12 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
_portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.bottom - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
_coverLabel.frame = CGRectMake(floor((_portraitToolsWrapperView.frame.size.width - _coverLabel.frame.size.width) / 2.0), CGRectGetMaxY(_scrubberView.frame) + 6.0, _coverLabel.frame.size.width, _coverLabel.frame.size.height);
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), screenEdges.top + floor((44.0 - _titleLabel.frame.size.height) / 2.0), _titleLabel.frame.size.width, _titleLabel.frame.size.height);
_cancelButton.frame = CGRectMake(screenEdges.left + 16.0, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
_doneButton.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - buttonSize.width) / 2.0), screenEdges.bottom - 56.0 - buttonSize.height, buttonSize.width, buttonSize.height);
}
break;
}

View File

@ -50,7 +50,7 @@
UIView *parentView = [self _parentView];
id<TGCaptionPanelView> inputPanel = nil;
if (_stickersContext) {
if (_stickersContext && _stickersContext.captionPanelView != nil) {
inputPanel = _stickersContext.captionPanelView();
}
_inputPanel = inputPanel;

View File

@ -1145,6 +1145,11 @@
return _intent & (TGPhotoEditorControllerForumAvatarIntent);
}
- (bool)presentedForSuggestedAvatar
{
return _intent & (TGPhotoEditorControllerSuggestedAvatarIntent);
}
#pragma mark - Transition
- (void)transitionIn
@ -1317,6 +1322,10 @@
TGPhotoEditorBackButton backButtonType = TGPhotoEditorBackButtonCancel;
TGPhotoEditorDoneButton doneButtonType = TGPhotoEditorDoneButtonCheck;
if ([self presentedForSuggestedAvatar]) {
[_portraitToolbarView setCancelDoneButtonsHidden:tab == TGPhotoEditorCropTab animated:!isInitialAppearance];
}
__weak TGPhotoEditorController *weakSelf = self;
TGPhotoEditorTabController *controller = nil;
switch (tab)
@ -1332,7 +1341,8 @@
{
bool skipInitialTransition = (![self presentedFromCamera] && self.navigationController != nil) || self.skipInitialTransition;
TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation]];
TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar]];
cropController.stickersContext = _stickersContext;
cropController.scrubberView = _scrubberView;
cropController.dotImageView = _dotImageView;
cropController.dotMarkerView = _dotMarkerView;
@ -1460,6 +1470,20 @@
[strongSelf returnFullPreviewView];
};
cropController.cancelPressed = ^{
__strong TGPhotoEditorController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_portraitToolbarView.cancelPressed();
};
cropController.donePressed = ^{
__strong TGPhotoEditorController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_portraitToolbarView.donePressed();
};
controller = cropController;
doneButtonType = TGPhotoEditorDoneButtonDone;

View File

@ -25,6 +25,8 @@
UILongPressGestureRecognizer *_longPressGestureRecognizer;
bool _transitionedOut;
bool _animatingCancelDoneButtons;
}
@end
@ -71,6 +73,9 @@
- (void)setBackButtonType:(TGPhotoEditorBackButton)backButtonType {
_backButtonType = backButtonType;
if (_animatingCancelDoneButtons)
return;
UIImage *cancelImage = nil;
switch (backButtonType)
{
@ -88,6 +93,9 @@
- (void)setDoneButtonType:(TGPhotoEditorDoneButton)doneButtonType {
_doneButtonType = doneButtonType;
if (_animatingCancelDoneButtons)
return;
TGMediaAssetsPallete *pallete = nil;
if ([_context respondsToSelector:@selector(mediaAssetsPallete)])
pallete = [_context mediaAssetsPallete];
@ -528,6 +536,48 @@
}
}
- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated {
CGFloat targetAlpha = hidden ? 0.0f : 1.0f;
if (animated)
{
_animatingCancelDoneButtons = hidden;
if (hidden) {
_cancelButton.modernHighlight = false;
}
_cancelButton.hidden = false;
_doneButton.hidden = false;
[UIView animateWithDuration:0.2f
animations:^
{
_cancelButton.alpha = targetAlpha;
_doneButton.alpha = targetAlpha;
} completion:^(__unused BOOL finished)
{
_animatingCancelDoneButtons = false;
_cancelButton.hidden = hidden;
_doneButton.hidden = hidden;
if (hidden) {
_cancelButton.modernHighlight = true;
}
if (hidden) {
[self setBackButtonType:_backButtonType];
[self setDoneButtonType:_doneButtonType];
}
}];
}
else
{
_cancelButton.alpha = targetAlpha;
_doneButton.alpha = targetAlpha;
_cancelButton.hidden = hidden;
_doneButton.hidden = hidden;
}
}
- (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab
{
for (TGPhotoEditorButton *button in _buttonsWrapperView.subviews)

View File

@ -12,7 +12,7 @@
@implementation TGPhotoVideoEditor
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed
{
id<LegacyComponentsOverlayWindowManager> windowManager = [context makeOverlayWindowManager];
@ -34,20 +34,31 @@
}
void (^present)(UIImage *) = ^(UIImage *screenImage) {
TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab];
// controller.stickersContext = _stickersContext;
controller.skipInitialTransition = true;
controller.dontHideStatusBar = true;
TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSuggestedAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab];
controller.stickersContext = stickersContext;
TGMediaAvatarEditorTransition *transition;
if (transitionView != nil) {
transition = [[TGMediaAvatarEditorTransition alloc] initWithController:controller fromView:transitionView];
} else {
controller.skipInitialTransition = true;
controller.dontHideStatusBar = true;
}
controller.didFinishEditing = ^(__unused id<TGMediaEditAdjustments> adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges, void(^commit)(void))
{
if (didFinishWithImage != nil)
didFinishWithImage(resultImage);
commit();
};
controller.didFinishEditingVideo = ^(AVAsset *asset, id<TGMediaEditAdjustments> adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) {
if (didFinishWithVideo != nil) {
if ([asset isKindOfClass:[AVURLAsset class]]) {
didFinishWithVideo(resultImage, [(AVURLAsset *)asset URL], adjustments);
}
commit();
}
};
controller.requestThumbnailImage = ^(id<TGMediaEditableItem> editableItem)
@ -80,6 +91,46 @@
TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:controller];
controllerWindow.hidden = false;
controller.view.clipsToBounds = true;
if (transitionView != nil) {
transition.referenceFrame = ^CGRect
{
UIView *referenceView = transitionView;
return [referenceView.superview convertRect:referenceView.frame toView:nil];
};
transition.referenceImageSize = ^CGSize
{
return image.size;
};
transition.referenceScreenImageSignal = ^SSignal *
{
return [SSignal single:image];
};
[transition presentAnimated:true];
transitionView.alpha = 0.0;
TGDispatchAfter(0.4, dispatch_get_main_queue(), ^{
transitionView.alpha = 1.0;
});
controller.beginCustomTransitionOut = ^(CGRect outReferenceFrame, UIView *repView, void (^completion)(void))
{
transition.outReferenceFrame = outReferenceFrame;
transition.repView = repView;
transitionView.alpha = 0.0;
[transition dismissAnimated:true completion:^
{
transitionView.alpha = 1.0;
dispatch_async(dispatch_get_main_queue(), ^
{
if (completion != nil)
completion();
dismissed();
});
}];
};
}
};
if (image != nil) {

View File

@ -29,6 +29,7 @@ swift_library(
"//submodules/TextFormat:TextFormat",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/DrawingUI:DrawingUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
visibility = [
"//visibility:public",

View File

@ -2,9 +2,13 @@ import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import LegacyUI
import AccountContext
import SaveToCameraRoll
public func presentLegacyAvatarPicker(holder: Atomic<NSObject?>, signup: Bool, theme: PresentationTheme, present: (ViewController, Any?) -> Void, openCurrent: (() -> Void)?, completion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { _, _, _ in}) {
let legacyController = LegacyController(presentation: .custom, theme: theme)
@ -47,3 +51,58 @@ public func presentLegacyAvatarPicker(holder: Atomic<NSObject?>, signup: Bool, t
}
}
}
public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) {
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: media)
|> deliverOnMainQueue).start(next: { (value, isImage) in
guard case let .data(data) = value, data.complete else {
return
}
var image: UIImage?
var url: URL?
if let maybeImage = UIImage(contentsOfFile: data.path) {
image = maybeImage
} else if data.complete {
url = URL(fileURLWithPath: data.path)
}
if image == nil && url == nil {
return
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
legacyController.enableSizeClassSignal = true
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, transitionView: transitionView, didFinishWithImage: { image in
if let image = image {
imageCompletion(image)
}
}, didFinishWithVideo: { image, url, adjustments in
if let image = image, let url = url, let adjustments = adjustments {
videoCompletion(image, url, adjustments)
}
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
})
}

View File

@ -199,12 +199,14 @@ private enum LegacyAssetItem {
private final class LegacyAssetItemWrapper: NSObject {
let item: LegacyAssetItem
let timer: Int?
let spoiler: Bool?
let groupedId: Int64?
let uniqueId: String?
init(item: LegacyAssetItem, timer: Int?, groupedId: Int64?, uniqueId: String?) {
init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, groupedId: Int64?, uniqueId: String?) {
self.item = item
self.timer = timer
self.spoiler = spoiler
self.groupedId = groupedId
self.uniqueId = uniqueId
@ -232,10 +234,10 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.path
if let url = url {
let dimensions = image.size
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
}
} else {
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
}
return result
} else if (dict["type"] as! NSString) == "cloudPhoto" {
@ -256,9 +258,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
name = customName
}
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
} else {
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
}
return result
} else if (dict["type"] as! NSString) == "file" {
@ -279,12 +281,12 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue!
let duration = (dict["duration"]! as AnyObject).doubleValue!
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
return result
}
var result: [AnyHashable: Any] = [:]
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
return result
}
} else if (dict["type"] as! NSString) == "video" {
@ -296,13 +298,13 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
if let asset = dict["asset"] as? TGMediaAsset {
var result: [AnyHashable: Any] = [:]
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
return result
} else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString {
let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue!
let duration = (dict["duration"]! as AnyObject).doubleValue!
var result: [AnyHashable: Any] = [:]
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
return result
}
} else if (dict["type"] as! NSString) == "cameraVideo" {
@ -318,7 +320,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
let dimensions = previewImage.pixelSize()
let duration = (dict["duration"]! as AnyObject).doubleValue!
var result: [AnyHashable: Any] = [:]
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
return result
}
}
@ -467,6 +469,9 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
if let timer = item.timer, timer > 0 && timer <= 60 {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
}
if let spoiler = item.spoiler, spoiler {
attributes.append(MediaSpoilerMessageAttribute())
}
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
@ -509,7 +514,10 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
if let timer = item.timer, timer > 0 && timer <= 60 {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
}
if let spoiler = item.spoiler, spoiler {
attributes.append(MediaSpoilerMessageAttribute())
}
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
if !entities.isEmpty {
@ -751,6 +759,9 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -
if let timer = item.timer, timer > 0 && timer <= 60 {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
}
if let spoiler = item.spoiler, spoiler {
attributes.append(MediaSpoilerMessageAttribute())
}
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))

View File

@ -9,6 +9,7 @@ import TelegramAnimatedStickerNode
import YuvConversion
import StickerResources
import DrawingUI
import SolidRoundedButtonNode
protocol LegacyPaintEntity {
var position: CGPoint { get }
@ -496,4 +497,17 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
public func drawingAdapter(_ size: CGSize) -> TGPhotoDrawingAdapter! {
return LegacyDrawingAdapter(context: self.context, size: size)
}
public func solidRoundedButton(_ title: String!, action: (() -> Void)!) -> (UIView & TGPhotoSolidRoundedButtonView)! {
let theme = SolidRoundedButtonTheme(theme: self.context.sharedContext.currentPresentationData.with { $0 }.theme)
let button = SolidRoundedButtonView(title: title, theme: theme, height: 50.0, cornerRadius: 10.0)
button.pressed = action
return button
}
}
extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView {
public func updateWidth(_ width: CGFloat) {
let _ = self.updateLayout(width: width, transition: .immediate)
}
}

View File

@ -41,6 +41,7 @@ swift_library(
"//submodules/SparseItemGrid:SparseItemGrid",
"//submodules/UndoUI:UndoUI",
"//submodules/MoreButtonNode:MoreButtonNode",
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
],
visibility = [
"//visibility:public",

View File

@ -12,6 +12,9 @@ import Photos
import CheckNode
import LegacyComponents
import PhotoResources
import InvisibleInkDustNode
import ImageBlur
import FastBlur
enum MediaPickerGridItemContent: Equatable {
case asset(PHFetchResult<PHAsset>, Int)
@ -87,6 +90,9 @@ final class MediaPickerGridItemNode: GridItemNode {
private var interaction: MediaPickerInteraction?
private var theme: PresentationTheme?
private let spoilerDisposable = MetaDisposable()
var spoilerNode: SpoilerOverlayNode?
private var currentIsPreviewing = false
var selected: (() -> Void)?
@ -112,6 +118,14 @@ final class MediaPickerGridItemNode: GridItemNode {
super.init()
self.addSubnode(self.imageNode)
self.imageNode.contentUpdated = { [weak self] image in
self?.spoilerNode?.setImage(image)
}
}
deinit {
self.spoilerDisposable.dispose()
}
var identifier: String {
@ -170,17 +184,20 @@ final class MediaPickerGridItemNode: GridItemNode {
let wasHidden = self.isHidden
self.isHidden = self.interaction?.hiddenMediaId == self.identifier
if !self.isHidden && wasHidden {
self.animateFadeIn(animateCheckNode: true)
self.animateFadeIn(animateCheckNode: true, animateSpoilerNode: true)
}
}
func animateFadeIn(animateCheckNode: Bool) {
func animateFadeIn(animateCheckNode: Bool, animateSpoilerNode: Bool) {
if animateCheckNode {
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if animateSpoilerNode {
self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
override func didLoad() {
@ -298,6 +315,31 @@ final class MediaPickerGridItemNode: GridItemNode {
}
self.imageNode.setSignal(imageSignal)
let spoilerSignal = Signal<Bool, NoError> { subscriber in
if let signal = editingContext.spoilerSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
if let next = next as? Bool {
subscriber.putNext(next)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
self.spoilerDisposable.set((spoilerSignal
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in
guard let strongSelf = self else {
return
}
strongSelf.updateHasSpoiler(hasSpoiler)
}))
if asset.mediaType == .video {
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo")
@ -331,6 +373,36 @@ final class MediaPickerGridItemNode: GridItemNode {
self.updateHiddenMedia()
}
private var didSetupSpoiler = false
private func updateHasSpoiler(_ hasSpoiler: Bool) {
var animated = true
if !self.didSetupSpoiler {
animated = false
self.didSetupSpoiler = true
}
if hasSpoiler {
if self.spoilerNode == nil {
let spoilerNode = SpoilerOverlayNode()
self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode)
self.spoilerNode = spoilerNode
spoilerNode.setImage(self.imageNode.image)
if animated {
spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
self.spoilerNode?.update(size: self.bounds.size, transition: .immediate)
self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size)
} else if let spoilerNode = self.spoilerNode {
self.spoilerNode = nil
spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
spoilerNode?.removeFromSupernode()
})
}
}
override func layout() {
super.layout()
@ -345,6 +417,11 @@ final class MediaPickerGridItemNode: GridItemNode {
let checkSize = CGSize(width: 29.0, height: 29.0)
self.checkNode?.frame = CGRect(origin: CGPoint(x: self.bounds.width - checkSize.width - 3.0, y: 3.0), size: checkSize)
if let spoilerNode = self.spoilerNode, self.bounds.width > 0.0 {
spoilerNode.frame = self.bounds
spoilerNode.update(size: self.bounds.size, transition: .immediate)
}
}
func transitionView() -> UIView {
@ -361,3 +438,79 @@ final class MediaPickerGridItemNode: GridItemNode {
}
}
class SpoilerOverlayNode: ASDisplayNode {
private let blurNode: ASImageNode
let dustNode: MediaDustNode
private var maskView: UIView?
private var maskLayer: CAShapeLayer?
override init() {
self.blurNode = ASImageNode()
self.blurNode.displaysAsynchronously = false
self.dustNode = MediaDustNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.blurNode)
self.addSubnode(self.dustNode)
}
override func didLoad() {
super.didLoad()
let maskView = UIView()
self.maskView = maskView
// self.dustNode.view.mask = maskView
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.fillColor = UIColor.white.cgColor
maskView.layer.addSublayer(maskLayer)
self.maskLayer = maskLayer
}
func setImage(_ image: UIImage?) {
self.blurNode.image = image.flatMap { blurredImage($0) }
}
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.blurNode, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(node: self.dustNode, frame: CGRect(origin: .zero, size: size))
self.dustNode.update(size: size, color: .white, transition: transition)
}
}
private func blurredImage(_ image: UIImage) -> UIImage? {
guard let image = image.cgImage else {
return nil
}
let thumbnailSize = CGSize(width: image.width, height: image.height)
let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0))
if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) {
thumbnailContext.withFlippedContext { c in
c.interpolationQuality = .none
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize))
}
imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes)
let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0))
if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) {
thumbnailContext2.withFlippedContext { c in
c.interpolationQuality = .none
if let image = thumbnailContext.generateImage()?.cgImage {
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size))
}
}
imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes)
adjustSaturationInContext(context: thumbnailContext2, saturation: 1.7)
return thumbnailContext2.generateImage()
}
}
return nil
}

View File

@ -599,8 +599,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
if let node = node {
return (node.view, { [weak node] animateCheckNode in
node?.animateFadeIn(animateCheckNode: animateCheckNode)
return (node.view, node.spoilerNode?.dustNode, { [weak node] animateCheckNode in
node?.animateFadeIn(animateCheckNode: animateCheckNode, animateSpoilerNode: false)
})
} else {
return nil
@ -1516,21 +1516,33 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let strings = self.presentationData.strings
let selectionCount = self.selectionCount
var hasSpoilers = false
if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState {
for case let item as TGMediaEditableItem in selectionContext.selectedItems() {
if editingContext.spoiler(for: item) {
hasSpoilers = true
break
}
}
}
let items: Signal<ContextController.Items, NoError> = self.groupedPromise.get()
|> deliverOnMainQueue
|> map { [weak self] grouped -> ContextController.Items in
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {})
})))
if !hasSpoilers {
items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {})
})))
}
if selectionCount > 1 {
items.append(.separator)
if !items.isEmpty {
items.append(.separator)
}
items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in
if !grouped {
return nil
@ -1552,6 +1564,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.groupedValue = false
})))
}
if !items.isEmpty {
items.append(.separator)
}
items.append(.action(ContextMenuActionItem(text: hasSpoilers ? "Disable Spoiler Effect" : "Spoiler Effect", icon: { _ in return nil }, animationName: "anim_spoiler", action: { [weak self] _, f in
f(.default)
guard let strongSelf = self else {
return
}
if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState {
for case let item as TGMediaEditableItem in selectionContext.selectedItems() {
editingContext.setSpoiler(!hasSpoilers, for: item)
}
}
})))
return ContextController.Items(content: .list(items))
}

View File

@ -25,6 +25,9 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
private var adjustmentsDisposable: Disposable?
private let spoilerDisposable = MetaDisposable()
private var spoilerNode: SpoilerOverlayNode?
private var theme: PresentationTheme?
private var validLayout: CGSize?
@ -68,43 +71,75 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
self.addSubnode(self.imageNode)
if asset.isVideo, let editingState = interaction?.editingState {
func adjustmentsChangedSignal(editingState: TGMediaEditingContext) -> Signal<TGMediaEditAdjustments?, NoError> {
return Signal { subscriber in
let disposable = editingState.adjustmentsSignal(for: asset).start(next: { next in
if let next = next as? TGMediaEditAdjustments {
subscriber.putNext(next)
} else if next == nil {
subscriber.putNext(nil)
if let editingState = interaction?.editingState {
if asset.isVideo {
func adjustmentsChangedSignal(editingState: TGMediaEditingContext) -> Signal<TGMediaEditAdjustments?, NoError> {
return Signal { subscriber in
let disposable = editingState.adjustmentsSignal(for: asset).start(next: { next in
if let next = next as? TGMediaEditAdjustments {
subscriber.putNext(next)
} else if next == nil {
subscriber.putNext(nil)
}
}, error: nil, completed: {})
return ActionDisposable {
disposable?.dispose()
}
}, error: nil, completed: {})
return ActionDisposable {
disposable?.dispose()
}
}
self.adjustmentsDisposable = (adjustmentsChangedSignal(editingState: editingState)
|> deliverOnMainQueue).start(next: { [weak self] adjustments in
if let strongSelf = self {
let duration: Double
if let adjustments = adjustments as? TGVideoEditAdjustments, adjustments.trimApplied() {
duration = adjustments.trimEndValue - adjustments.trimStartValue
} else {
duration = asset.originalDuration ?? 0.0
}
strongSelf.videoDuration = duration
if let size = strongSelf.validLayout {
strongSelf.updateLayout(size: size, transition: .immediate)
}
}
})
}
let spoilerSignal = Signal<Bool, NoError> { subscriber in
if let signal = editingState.spoilerSignal(forIdentifier: asset.uniqueIdentifier) {
let disposable = signal.start(next: { next in
if let next = next as? Bool {
subscriber.putNext(next)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
self.adjustmentsDisposable = (adjustmentsChangedSignal(editingState: editingState)
|> deliverOnMainQueue).start(next: { [weak self] adjustments in
if let strongSelf = self {
let duration: Double
if let adjustments = adjustments as? TGVideoEditAdjustments, adjustments.trimApplied() {
duration = adjustments.trimEndValue - adjustments.trimStartValue
} else {
duration = asset.originalDuration ?? 0.0
}
strongSelf.videoDuration = duration
if let size = strongSelf.validLayout {
strongSelf.updateLayout(size: size, transition: .immediate)
}
self.spoilerDisposable.set((spoilerSignal
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in
guard let strongSelf = self else {
return
}
})
strongSelf.updateHasSpoiler(hasSpoiler)
}))
}
self.imageNode.contentUpdated = { [weak self] image in
self?.spoilerNode?.setImage(image)
}
}
deinit {
self.adjustmentsDisposable?.dispose()
self.spoilerDisposable.dispose()
}
override func didLoad() {
@ -120,6 +155,36 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
self.interaction?.openSelectedMedia(asset, self.imageNode.image)
}
private var didSetupSpoiler = false
private func updateHasSpoiler(_ hasSpoiler: Bool) {
var animated = true
if !self.didSetupSpoiler {
animated = false
self.didSetupSpoiler = true
}
if hasSpoiler {
if self.spoilerNode == nil {
let spoilerNode = SpoilerOverlayNode()
self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode)
self.spoilerNode = spoilerNode
spoilerNode.setImage(self.imageNode.image)
if animated {
spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
self.spoilerNode?.update(size: self.bounds.size, transition: .immediate)
self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size)
} else if let spoilerNode = self.spoilerNode {
self.spoilerNode = nil
spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
spoilerNode?.removeFromSupernode()
})
}
}
func setup(size: CGSize) {
let editingState = self.interaction?.editingState
let editedSignal = Signal<UIImage?, NoError> { subscriber in
@ -229,6 +294,10 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
if let durationBackgroundNode = self.durationBackgroundNode, durationBackgroundNode.alpha > 0.0 {
durationBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if let spoilerNode = self.spoilerNode, spoilerNode.alpha > 0.0 {
spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
@ -249,6 +318,11 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
if let spoilerNode = self.spoilerNode {
transition.updateFrame(node: spoilerNode, frame: CGRect(origin: CGPoint(), size: size))
spoilerNode.update(size: size, transition: transition)
}
let checkSize = CGSize(width: 29.0, height: 29.0)
if let checkNode = self.checkNode {
transition.updateFrame(node: checkNode, frame: CGRect(origin: CGPoint(x: size.width - checkSize.width - 3.0, y: 3.0), size: checkSize))
@ -320,7 +394,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
})
}
func animateTo(_ view: UIView, completion: @escaping (Bool) -> Void) {
func animateTo(_ view: UIView, dustNode: ASDisplayNode?, completion: @escaping (Bool) -> Void) {
view.alpha = 0.0
let frame = self.frame
@ -331,6 +405,20 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
self.durationTextNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
var dustSupernode: ASDisplayNode?
var dustPosition: CGPoint?
if let dustNode = dustNode {
dustSupernode = dustNode.supernode
dustPosition = dustNode.position
self.addSubnode(dustNode)
dustNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
dustNode.layer.animatePosition(from: CGPoint(x: frame.width / 2.0, y: frame.height / 2.0), to: dustNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.spoilerNode?.dustNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
self.corners = []
self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak view, weak self] _ in
@ -339,6 +427,11 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
self?.durationTextNode?.layer.removeAllAnimations()
self?.durationBackgroundNode?.layer.removeAllAnimations()
if let dustNode = dustNode {
dustSupernode?.addSubnode(dustNode)
dustNode.position = dustPosition ?? dustNode.position
}
var animateCheckNode = false
if let strongSelf = self, let checkNode = strongSelf.checkNode, checkNode.alpha.isZero {
animateCheckNode = true
@ -350,6 +443,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
Queue.mainQueue().after(0.01) {
self?.layer.removeAllAnimations()
self?.spoilerNode?.dustNode.layer.removeAllAnimations()
}
})
}
@ -479,7 +573,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
})
}
var getTransitionView: (String ) -> (UIView, (Bool) -> Void)? = { _ in return nil }
var getTransitionView: (String) -> (UIView, ASDisplayNode?, (Bool) -> Void)? = { _ in return nil }
func animateIn(initiated: @escaping () -> Void, completion: @escaping () -> Void = {}) {
let _ = (self.ready.get()
@ -502,7 +596,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
}
for (identifier, itemNode) in strongSelf.itemNodes {
if let (transitionView, _) = strongSelf.getTransitionView(identifier) {
if let (transitionView, _, _) = strongSelf.getTransitionView(identifier) {
itemNode.animateFrom(transitionView)
} else {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
@ -550,18 +644,22 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
}
for (identifier, itemNode) in self.itemNodes {
if let (transitionView, completion) = self.getTransitionView(identifier) {
itemNode.animateTo(transitionView, completion: completion)
if let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) {
itemNode.animateTo(transitionView, dustNode: maybeDustNode, completion: completion)
} else {
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false)
}
}
self.messageNodes?.first?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.messageNodes?.first?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
if let topNode = self.messageNodes?.first {
topNode.layer.animateAlpha(from: topNode.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
topNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
self.messageNodes?.last?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.messageNodes?.last?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
if let bottomNode = self.messageNodes?.last {
bottomNode.layer.animateAlpha(from: bottomNode.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
bottomNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

View File

@ -195,10 +195,16 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
subject = .image(entry.representations)
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
}
let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll)
shareController.actionCompleted = { [weak self] in
if let strongSelf = self, let actionCompletionText = actionCompletionText {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var forceTheme: PresentationTheme?
if !presentationData.theme.overallDarkAppearance {
forceTheme = defaultDarkColorPresentationTheme
}
let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme)
shareController.actionCompleted = {
if let actionCompletionText = actionCompletionText {
interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil)
}
}

View File

@ -19,6 +19,8 @@ import AppBundle
import MusicAlbumArtResources
import Svg
import RangeSet
import Accelerate
private enum ResourceFileData {
case data(Data)
@ -1146,6 +1148,8 @@ public func chatSecretPhoto(account: Account, photoReference: ImageMediaReferenc
}
}
adjustSaturationInContext(context: context, saturation: 1.7)
addCorners(context, arguments: arguments)
return context
@ -1153,6 +1157,45 @@ public func chatSecretPhoto(account: Account, photoReference: ImageMediaReferenc
}
}
public func adjustSaturationInContext(context: DrawingContext, saturation: CGFloat) {
var buffer = vImage_Buffer()
buffer.data = context.bytes
buffer.width = UInt(context.size.width * context.scale)
buffer.height = UInt(context.size.height * context.scale)
buffer.rowBytes = context.bytesPerRow
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation = saturation
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
}
private func avatarGalleryThumbnailDatas(postbox: Postbox, representations: [ImageRepresentationWithReference], fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, synchronousLoad: Bool) -> Signal<Tuple3<Data?, Data?, Bool>, NoError> {
if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = imageRepresentationLargerThan(representations.map({ $0.representation }), size: PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) {
let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: synchronousLoad)

View File

@ -165,6 +165,10 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me
if let image = image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) {
return result
}
case let .suggestedProfilePhoto(image):
if let image = image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) {
return result
}
default:
break
}

View File

@ -225,7 +225,11 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state
if let peer = transaction.getPeer(peer.id) {
updatePeers(transaction: transaction, peers: [peer], update: { (_, peer) -> Peer? in
if let peer = peer as? TelegramUser {
return peer.withUpdatedPhoto(representations)
if customPeerPhotoMode == .suggest {
return peer
} else {
return peer.withUpdatedPhoto(representations)
}
} else {
return peer
}

View File

@ -825,8 +825,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicIconChanged(".")._tuple, body: bodyAttributes, argumentAttributes: [0: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])])
}
}
case .suggestedProfilePhoto:
attributedString = nil
case let .suggestedProfilePhoto(image):
if (image?.videoRepresentations.isEmpty ?? true) {
attributedString = NSAttributedString(string: strings.Notification_SuggestedProfilePhoto, font: titleFont, textColor: primaryTextColor)
} else {
attributedString = NSAttributedString(string: strings.Notification_SuggestedProfileVideo, font: titleFont, textColor: primaryTextColor)
}
case .unknown:
attributedString = nil
}

View File

@ -471,7 +471,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
switch titleContent {
case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _, customMessageCount):
if let customMessageCount = customMessageCount, customMessageCount != 0 {
let string = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
let string = NSAttributedString(string: self.strings.Conversation_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
state = .info(string, .generic)
} else if let peer = peerViewMainPeer(peerView) {
let servicePeer = isServicePeer(peer)

File diff suppressed because one or more lines are too long

View File

@ -820,6 +820,38 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration))
strongSelf.push(controller)
return true
case let .suggestedProfilePhoto(image):
if let image = image {
if message.effectivelyIncoming(strongSelf.context.account.peerId) {
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: message.id, media: image) {
selectedNode = result
}
}
}
let transitionView = selectedNode?.0.view
legacyAvatarEditor(context: strongSelf.context, media: .message(message: MessageReference(message), media: image), transitionView: transitionView, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, imageCompletion: { [weak self] image in
if let strongSelf = self {
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
settingsController.updateProfilePhoto(image)
}
}
}, videoCompletion: { [weak self] image, url, adjustments in
if let strongSelf = self {
if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl {
settingsController.updateProfileVideo(image, asset: AVURLAsset(url: url), adjustments: adjustments)
}
}
})
} else {
openMessageByAction = true
}
}
default:
break
}

View File

@ -110,6 +110,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if case .giftPremium = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if case .suggestedProfilePhoto = action.action {
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}

View File

@ -174,6 +174,7 @@ extension UIBezierPath {
}
private class ExtendedMediaOverlayNode: ASDisplayNode {
private let blurredImageNode: TransformImageNode
private let dustNode: MediaDustNode
private let buttonNode: HighlightTrackingButtonNode
private let highlightedBackgroundNode: ASDisplayNode
@ -184,6 +185,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
private var maskLayer: CAShapeLayer?
override init() {
self.blurredImageNode = TransformImageNode()
self.dustNode = MediaDustNode()
self.buttonNode = HighlightTrackingButtonNode()
@ -202,10 +205,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
self.textNode = ImmediateTextNode()
super.init()
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubnode(self.blurredImageNode)
self.addSubnode(self.dustNode)
self.addSubnode(self.buttonNode)
@ -250,22 +251,51 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
self.maskLayer = maskLayer
}
func update(size: CGSize, text: String, corners: ImageCorners?) {
func update(size: CGSize, text: String, imageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize, CGSize)?, imageFrame: CGRect, corners: ImageCorners?) {
let spacing: CGFloat = 2.0
let padding: CGFloat = 10.0
if let (imageSignal, drawingSize, boundingSize) = imageSignal {
self.blurredImageNode.setSignal(imageSignal)
let imageLayout = self.blurredImageNode.asyncLayout()
let arguments = TransformImageArguments(corners: corners ?? ImageCorners(), imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .blurBackground, emptyColor: .clear, custom: nil)
let apply = imageLayout(arguments)
apply()
self.blurredImageNode.isHidden = false
self.isUserInteractionEnabled = !self.dustNode.isRevealed
self.dustNode.revealed = { [weak self] in
self?.blurredImageNode.removeFromSupernode()
self?.isUserInteractionEnabled = false
}
} else {
self.blurredImageNode.isHidden = true
self.isUserInteractionEnabled = false
}
self.blurredImageNode.frame = imageFrame
self.dustNode.frame = CGRect(origin: .zero, size: size)
self.dustNode.update(size: size, color: .white)
self.dustNode.update(size: size, color: .white, transition: .immediate)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .center)
let textSize = self.textNode.updateLayout(size)
if let iconSize = self.iconNode.image?.size {
let contentSize = CGSize(width: iconSize.width + textSize.width + spacing + padding * 2.0, height: 32.0)
self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize)
self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize)
self.iconNode.frame = CGRect(origin: CGPoint(x: self.buttonNode.frame.minX + padding, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize)
self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize)
if text.isEmpty {
self.buttonNode.isHidden = true
self.textNode.isHidden = true
} else {
self.buttonNode.isHidden = false
self.textNode.isHidden = false
self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .center)
let textSize = self.textNode.updateLayout(size)
if let iconSize = self.iconNode.image?.size {
let contentSize = CGSize(width: iconSize.width + textSize.width + spacing + padding * 2.0, height: 32.0)
self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize)
self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize)
self.iconNode.frame = CGRect(origin: CGPoint(x: self.buttonNode.frame.minX + padding, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize)
self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize)
}
}
var leftOffset: CGFloat = 0.0
@ -290,6 +320,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
private let imageNode: TransformImageNode
private var currentImageArguments: TransformImageArguments?
private var currentHighQualityImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize)?
private var currentBlurredImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize, CGSize)?
private var highQualityImageNode: TransformImageNode?
private var videoNode: UniversalVideoNode?
@ -855,6 +886,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
var updateImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)?
var updateBlurredImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)?
var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus?), NoError>?
var updatedFetchControls: FetchControls?
@ -946,6 +978,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
updateImageSignal = { synchronousLoad, highQuality in
return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality)
}
updateBlurredImageSignal = { synchronousLoad, _ in
return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image))
}
}
updatedFetchControls = FetchControls(fetch: { manual in
@ -1323,9 +1358,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
if let imageDimensions = imageDimensions {
strongSelf.currentHighQualityImageSignal = (updateImageSignal(false, true), imageDimensions)
if let updateBlurredImageSignal = updateBlurredImageSignal {
strongSelf.currentBlurredImageSignal = (updateBlurredImageSignal(false, true), drawingSize, boundingSize)
}
}
}
if let _ = secretBeginTimeAndTimeout {
if updatedStatusSignal == nil, let fetchStatus = strongSelf.fetchStatus, case .Local = fetchStatus {
if let statusNode = strongSelf.statusNode, case .secretTimeout = statusNode.state {
@ -1842,7 +1881,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
badgeNode.removeFromSupernode()
}
var displaySpoiler = false
if let invoice = invoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia {
displaySpoiler = true
} else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) {
displaySpoiler = true
}
if displaySpoiler {
if self.extendedMediaOverlayNode == nil {
let extendedMediaOverlayNode = ExtendedMediaOverlayNode()
self.extendedMediaOverlayNode = extendedMediaOverlayNode
@ -1864,7 +1910,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
break
}
}
self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, corners: self.currentImageArguments?.corners)
self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners)
} else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode {
self.extendedMediaOverlayNode = nil
extendedMediaOverlayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak extendedMediaOverlayNode] _ in

View File

@ -13,18 +13,22 @@ import LocalizedPeerData
import TelegramStringFormatting
import WallpaperBackgroundNode
import ReactionSelectionNode
import PhotoResources
class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let titleNode: TextNode
private let subtitleNode: TextNode
private let imageNode: TransformImageNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonStarsNode: PremiumStarsNode
private let buttonTitleNode: TextNode
private var absoluteRect: (CGRect, CGSize)?
private let fetchDisposable = MetaDisposable()
required init() {
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
@ -39,6 +43,8 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.imageNode = TransformImageNode()
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
@ -54,6 +60,7 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.buttonStarsNode)
@ -82,6 +89,54 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id == messageId {
return (self.imageNode, self.imageNode.bounds, { [weak self] in
guard let strongSelf = self else {
return (nil, nil)
}
let resultView = strongSelf.imageNode.view.snapshotContentTree(unhide: true)
return (resultView, nil)
})
} else {
return nil
}
}
override func updateHiddenMedia(_ media: [Media]?) -> Bool {
var mediaHidden = false
var currentMedia: Media?
if let item = item {
mediaLoop: for media in item.message.media {
if let media = media as? TelegramMediaAction {
switch media.action {
case let .suggestedProfilePhoto(image):
currentMedia = image
break mediaLoop
default:
break
}
}
}
}
if let currentMedia = currentMedia, let media = media {
for item in media {
if item.isSemanticallyEqual(to: currentMedia) {
mediaHidden = true
break
}
}
}
self.imageNode.isHidden = mediaHidden
return mediaHidden
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
@ -91,31 +146,71 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeImageLayout = self.imageNode.asyncLayout()
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let currentItem = self.item
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let giftSize = CGSize(width: 220.0, height: 240.0)
let width: CGFloat = 220.0
let imageSize = CGSize(width: 100.0, height: 100.0)
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_Title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var photo: TelegramMediaImage?
if let media = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = media.action {
photo = image
}
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Subtitle", font: Font.regular(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var mediaUpdated = true
if let photo = photo, let media = currentItem?.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(maybeCurrentPhoto) = media.action, let currentPhoto = maybeCurrentPhoto {
mediaUpdated = !photo.isSemanticallyEqual(to: currentPhoto)
}
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let isVideo = !(photo?.videoRepresentations.isEmpty ?? true)
let fromYou = item.message.author?.id == item.context.account.peerId
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: isVideo ? item.presentationData.strings.Conversation_SuggestedVideoTitle : item.presentationData.strings.Conversation_SuggestedPhotoTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? ""
let text: String
if fromYou {
text = isVideo ? item.presentationData.strings.Conversation_SuggestedVideoTextYou(peerName).string : item.presentationData.strings.Conversation_SuggestedPhotoTextYou(peerName).string
} else {
text = isVideo ? item.presentationData.strings.Conversation_SuggestedVideoText(peerName).string : item.presentationData.strings.Conversation_SuggestedPhotoText(peerName).string
}
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.regular(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: isVideo ? item.presentationData.strings.Conversation_SuggestedVideoView : item.presentationData.strings.Conversation_SuggestedPhotoView, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 18.0)
let backgroundSize = CGSize(width: width, height: titleLayout.size.height + subtitleLayout.size.height + 182.0)
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: 16.0), size: giftSize)
if let photo = photo {
if mediaUpdated {
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: photo), displayAtSize: nil, storeToDownloadsPeerType: nil).start())
}
let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, photoReference: .message(message: MessageReference(item.message), media: photo), synchronousLoad: synchronousLoads)
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
let arguments = TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())
let apply = makeImageLayout(arguments)
apply()
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize)
}
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
@ -127,10 +222,10 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode
let _ = subtitleApply()
let _ = buttonTitleApply()
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 127.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY - 1.0), size: subtitleLayout.size)
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + 2.0), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)

View File

@ -4789,9 +4789,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
var canReport = true
if channel.isVerified {
canReport = false
}
if channel.adminRights != nil {
canReport = false
}
@ -6741,11 +6738,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
}
private func updateProfilePhoto(_ image: UIImage, mode: AvatarEditingMode) {
fileprivate func updateProfilePhoto(_ image: UIImage, mode: AvatarEditingMode) {
guard let data = image.jpegData(compressionQuality: 0.6) else {
return
}
if self.headerNode.isAvatarExpanded {
self.headerNode.ignoreCollapse = true
self.headerNode.updateIsAvatarExpanded(false, transition: .immediate)
@ -6757,7 +6754,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false)
self.state = self.state.withUpdatingAvatar(.image(representation))
if case .suggest = mode {
} else {
self.state = self.state.withUpdatingAvatar(.image(representation))
}
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false)
}
@ -6809,7 +6810,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}))
}
private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?, mode: AvatarEditingMode) {
fileprivate func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?, mode: AvatarEditingMode) {
guard let data = image.jpegData(compressionQuality: 0.6) else {
return
}
@ -6825,7 +6826,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false)
self.state = self.state.withUpdatingAvatar(.image(representation))
if case .suggest = mode {
} else {
self.state = self.state.withUpdatingAvatar(.image(representation))
}
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false)
}
@ -6964,7 +6969,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}))
}
private enum AvatarEditingMode {
fileprivate enum AvatarEditingMode {
case generic
case suggest
case custom
@ -9317,6 +9322,20 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
}
}
func updateProfilePhoto(_ image: UIImage) {
if !self.isNodeLoaded {
self.loadDisplayNode()
}
self.controllerNode.updateProfilePhoto(image, mode: .generic)
}
func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) {
if !self.isNodeLoaded {
self.loadDisplayNode()
}
self.controllerNode.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic)
}
static func displayChatNavigationMenu(context: AccountContext, chatNavigationStack: [ChatNavigationStackItem], nextFolderId: Int32?, parentController: ViewController, backButtonView: UIView, navigationController: NavigationController, gesture: ContextGesture) {
let peerMap = EngineDataMap(
Set(chatNavigationStack.map(\.peerId)).map(TelegramEngine.EngineData.Item.Peer.Peer.init)