diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index de38b84e22..4269b4ad29 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8479,3 +8479,9 @@ Sorry for the inconvenience."; "Conversation.SuggestedVideoText" = "%@ suggests you to use this video for your Telegram account."; "Conversation.SuggestedVideoTextYou" = "You suggested %@ to use this video for their Telegram account."; "Conversation.SuggestedVideoView" = "View"; + +"Conversation.Messages_1" = "%@ message"; +"Conversation.Messages_any" = "%@ messages"; + +"Notification.SuggestedProfilePhoto" = "Suggested Profile Photo"; +"Notification.SuggestedProfileVideo" = "Suggested Profile Video"; diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index af8992f0c9..bb6b6402b7 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -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 diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index c01105c60f..87ac307f0b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -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) diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index f0860edb01..6d9b1e33e8 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -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", diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 58e55b3238..eb19b29182 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -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 diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index d47d3fd15a..ec8760ac54 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -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) + } } }) } diff --git a/submodules/DrawingUI/Sources/DrawingGesture.swift b/submodules/DrawingUI/Sources/DrawingGesture.swift index 6bdbfb7f0f..0096844959 100644 --- a/submodules/DrawingUI/Sources/DrawingGesture.swift +++ b/submodules/DrawingUI/Sources/DrawingGesture.swift @@ -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 diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index f672b509a5..bf8698b3a6 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -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)), diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index 7cc7207b3e..1d007cac59 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -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 diff --git a/submodules/DrawingUI/Sources/PenTool.swift b/submodules/DrawingUI/Sources/PenTool.swift index 141e086184..e458cf143a 100644 --- a/submodules/DrawingUI/Sources/PenTool.swift +++ b/submodules/DrawingUI/Sources/PenTool.swift @@ -2,6 +2,73 @@ import Foundation import UIKit import Display +struct PointWeighted { + let point: CGPoint + let weight: CGFloat + + static let zero = PointWeighted(point: CGPoint.zero, weight: 0) +} + +struct LineSegment { + let start: CGPoint + let end: CGPoint + + var length: CGFloat { + return start.distance(to: end) + } + + func average(with line: LineSegment) -> LineSegment { + return LineSegment(start: start.average(with: line.start), end: end.average(with: line.end)) + } + + func normalLine(from weightedPoint: PointWeighted) -> LineSegment { + return normalLine(withMiddle: weightedPoint.point, weight: weightedPoint.weight) + } + + func normalLine(withMiddle middle: CGPoint, weight: CGFloat) -> LineSegment { + let relativeEnd = start.diff(to: end) + + guard weight != 0 && relativeEnd != CGPoint.zero else { + return LineSegment(start: middle, end: middle) + } + + let moddle = weight / 2 + let lengthK = moddle / length + + let k = CGPoint(x: relativeEnd.x * lengthK, y: relativeEnd.y * lengthK) + + var normalLineStart = CGPoint(x: k.y, y: -k.x) + var normalLineEnd = CGPoint(x: -k.y, y: k.x) + + normalLineStart.x += middle.x; + normalLineStart.y += middle.y; + + normalLineEnd.x += middle.x; + normalLineEnd.y += middle.y; + + return LineSegment(start: normalLineStart, end: normalLineEnd) + } +} + + +extension CGPoint { + func average(with point: CGPoint) -> CGPoint { + return CGPoint(x: (x + point.x) * 0.5, y: (y + point.y) * 0.5) + } + + func diff(to point: CGPoint) -> CGPoint { + return CGPoint(x: point.x - x, y: point.y - y) + } + + func forward(to point: CGPoint, by: CGFloat) -> CGPoint { + let diff = diff(to: point) + let distance = sqrt(pow(diff.x, 2) + pow(diff.y, 2)) + let k = by / distance + + return CGPoint(x: point.x + diff.x * k, y: point.y + diff.y * k) + } +} + final class PenTool: DrawingElement { class RenderLayer: SimpleLayer, DrawingRenderLayer { func setup(size: CGSize) { @@ -12,14 +79,34 @@ final class PenTool: DrawingElement { self.frame = bounds } - private var line: StrokeLine? - fileprivate func draw(line: StrokeLine, rect: CGRect) { - self.line = line + private var paths: [UIBezierPath] = [] + private var tempPath: UIBezierPath? + + private var color: UIColor? + fileprivate func draw(paths: [UIBezierPath], tempPath: UIBezierPath?, color: UIColor, rect: CGRect) { + self.paths = paths + self.tempPath = tempPath + self.color = color + self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0)) } override func draw(in ctx: CGContext) { - self.line?.drawInContext(ctx) + guard let color = self.color else { + return + } + + ctx.setFillColor(color.cgColor) + + for path in self.paths { + ctx.addPath(path.cgPath) + ctx.fillPath() + } + + if let tempPath = self.tempPath { + ctx.addPath(tempPath.cgPath) + ctx.fillPath() + } } } @@ -33,12 +120,12 @@ final class PenTool: DrawingElement { var path: Polyline? var boundingBox: CGRect? - private var renderLine: StrokeLine var didSetupArrow = false - private var renderLineArrow1: StrokeLine? - private var renderLineArrow2: StrokeLine? let renderLineWidth: CGFloat + var bezierPaths: [UIBezierPath] = [] + var tempBezierPath: UIBezierPath? + var translation = CGPoint() private var currentRenderLayer: DrawingRenderLayer? @@ -47,17 +134,20 @@ final class PenTool: DrawingElement { return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero } + var _points: [Polyline.Point] = [] + var points: [Polyline.Point] { - guard let linePath = self.path else { - return [] - } var points: [Polyline.Point] = [] - for point in linePath.points { + for point in self._points { points.append(point.offsetBy(self.translation)) } return points } + private let pointsPerLine: Int = 4 + private var nextPointIndex: Int = 0 + private var drawPoints = [PointWeighted](repeating: PointWeighted.zero, count: 4) + func containsPoint(_ point: CGPoint) -> Bool { return false // return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false @@ -89,11 +179,11 @@ final class PenTool: DrawingElement { self.renderLineWidth = lineWidth - self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth) - if arrow { - self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) - self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) - } +// self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth) +// if arrow { +// self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) +// self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) +// } } func setupRenderLayer() -> DrawingRenderLayer? { @@ -103,16 +193,49 @@ final class PenTool: DrawingElement { return layer } + func updateWithLocation(_ point: CGPoint, ended: Bool = false) { + if ended { + if let path = tempBezierPath { + bezierPaths.last?.append(path) + } + tempBezierPath = nil + nextPointIndex = 0 + } else { + addPoint(point) + } + } + func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { - guard case let .polyline(line) = path, let point = line.points.last else { + guard case let .location(point) = path else { return } - self.path = line + + self._points.append(point) - let rect = self.renderLine.draw(at: point) - if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { - currentRenderLayer.draw(line: self.renderLine, rect: rect) + switch state { + case .began: + addPoint(point.location) + case .changed: + if self._points.count > 1 { + self.updateTouchPoints(point: self._points[self._points.count - 1].location, previousPoint: self._points[self._points.count - 2].location) + self.updateWithLocation(point.location) + } + case .ended: + self.updateWithLocation(point.location, ended: true) + case .cancelled: + break } + + if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { + currentRenderLayer.draw(paths: self.bezierPaths, tempPath: self.tempBezierPath, color: self.color.toUIColor(), rect: CGRect(origin: .zero, size: self.drawingSize)) + } + +// self.path = line +// +// let rect = self.renderLine.draw(at: point) +// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { +// currentRenderLayer.draw(line: self.renderLine, rect: rect) +// } // self.path = bezierPath // if self.arrow && polyline.isComplete, polyline.points.count > 2 { @@ -143,286 +266,668 @@ final class PenTool: DrawingElement { // } } + private let minDistance: CGFloat = 2 + + private func addPoint(_ point: CGPoint) { + if isFirstPoint { + startNewLine(from: PointWeighted(point: point, weight: 2.0)) + } else { + let previousPoint = self.drawPoints[nextPointIndex - 1].point + guard previousPoint.distance(to: point) >= minDistance else { + return + } + if isStartOfNextLine { + finalizeBezier(nextLineStartPoint: point) + startNewLine(from: self.drawPoints[3]) + } + + let weightedPoint = PointWeighted(point: point, weight: weightForLine(between: previousPoint, and: point)) + addPoint(point: weightedPoint) + } + + let newBezier = generateBezierPath(withPointIndex: nextPointIndex - 1) + self.tempBezierPath = newBezier + } + + + private var isFirstPoint: Bool { + return nextPointIndex == 0 + } + + private var isStartOfNextLine: Bool { + return nextPointIndex >= pointsPerLine + } + + private func startNewLine(from weightedPoint: PointWeighted) { + drawPoints[0] = weightedPoint + nextPointIndex = 1 + } + + private func addPoint(point: PointWeighted) { + drawPoints[nextPointIndex] = point + nextPointIndex += 1 + } + + private func finalizeBezier(nextLineStartPoint: CGPoint) { + let touchPoint2 = drawPoints[2].point + let newTouchPoint3 = touchPoint2.average(with: nextLineStartPoint) + drawPoints[3] = PointWeighted(point: newTouchPoint3, weight: weightForLine(between: touchPoint2, and: newTouchPoint3)) + + guard let bezier = generateBezierPath(withPointIndex: 3) else { + return + } + self.bezierPaths.append(bezier) + + } + + private func generateBezierPath(withPointIndex index: Int) -> UIBezierPath? { + switch index { + case 0: + return UIBezierPath.dot(with: drawPoints[0]) + case 1: + return UIBezierPath.curve(withPointA: drawPoints[0], pointB: drawPoints[1]) + case 2: + return UIBezierPath.curve(withPointA: drawPoints[0], pointB: drawPoints[1], pointC: drawPoints[2]) + case 3: + return UIBezierPath.curve(withPointA: drawPoints[0], pointB: drawPoints[1], pointC: drawPoints[2], pointD: drawPoints[3]) + default: + return nil + } + } + + private func weightForLine(between pointA: CGPoint, and pointB: CGPoint) -> CGFloat { + let length = pointA.distance(to: pointB) + + let limitRange: CGFloat = 50 + + var lowerer: CGFloat = 0.2 + var constant: CGFloat = 2 + + let toolWidth = self.renderLineWidth + + constant = toolWidth - 3.0 + lowerer = 0.25 * toolWidth / 10.0 + + + let r = min(limitRange, length) + +// var r = limitRange - length +// if r < 0 { +// r = 0 +// } + +// print(r * lowerer) + + return (r * lowerer) + constant + } + + public var firstPoint: CGPoint = .zero + public var currentPoint: CGPoint = .zero + private var previousPoint: CGPoint = .zero + private var previousPreviousPoint: CGPoint = .zero + + private func setTouchPoints(point: CGPoint, previousPoint: CGPoint) { + self.previousPoint = previousPoint + self.previousPreviousPoint = previousPoint + self.currentPoint = point + } + + private func updateTouchPoints(point: CGPoint, previousPoint: CGPoint) { + self.previousPreviousPoint = self.previousPoint + self.previousPoint = previousPoint + self.currentPoint = point + } + + private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint { + return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5); + } + + private func getMidPoints() -> (CGPoint, CGPoint) { + let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint) + let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint) + return (mid1, mid2) + } + func draw(in context: CGContext, size: CGSize) { context.saveGState() context.translateBy(x: self.translation.x, y: self.translation.y) context.setShouldAntialias(true) - - if self.arrow, let path = self.path, let lastPoint = path.points.last { - var lastPointWithVelocity: Polyline.Point? - for point in path.points.reversed() { - if point.velocity > 0.0 { - lastPointWithVelocity = point - break - } - } - if !self.didSetupArrow, let lastPointWithVelocity = lastPointWithVelocity { - let w = self.renderLineWidth - var dist: CGFloat = 18.0 * sqrt(w) - let spread: CGFloat = .pi * max(0.05, 0.03 * sqrt(w)) - - let suffix = path.points.suffix(100).reversed() - - var p0 = suffix.first! - - var p2 = suffix.last! - var d: CGFloat = 0 - for p in suffix { - d += hypot(p0.location.x - p.location.x, p0.location.y - p.location.y) - if d >= dist { - p2 = p - break - } - p0 = p - } - - p0 = suffix.first! - dist = min(dist, hypot(p0.location.x - p2.location.x, p0.location.y - p2.location.y)) - - var i = 0 - for spread in [-spread, spread] { - var points: [CGPoint] = [] - points.append(lastPoint.location) - - p0 = suffix.first! - var prev = p0.location - d = 0 - for p in suffix { - let d1 = hypot(p0.location.x - p.location.x, p0.location.y - p.location.y) - d += d1 - if d >= dist { - break - } - let d2 = d1 / cos(spread) - let angle = atan2(p.location.y - p0.location.y, p.location.x - p0.location.x) - let cur = CGPoint(x: prev.x + d2 * cos(angle + spread), y: prev.y + d2 * sin(angle + spread)) - - points.append( - cur - ) - - p0 = p - prev = cur - } - - for point in points { - if i == 0 { - let _ = self.renderLineArrow1?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint)) - } else if i == 1 { - let _ = self.renderLineArrow2?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint)) - } - } - i += 1 - } - self.didSetupArrow = true - } - self.renderLineArrow1?.drawInContext(context) - self.renderLineArrow2?.drawInContext(context) + + context.setFillColor(self.color.toCGColor()) + for path in self.bezierPaths { + context.addPath(path.cgPath) + context.fillPath() } - self.renderLine.drawInContext(context) - context.restoreGState() } } -private class StrokeLine { - struct Segment { - let a: CGPoint - let b: CGPoint - let c: CGPoint - let d: CGPoint - let abWidth: CGFloat - let cdWidth: CGFloat +extension UIBezierPath { + + class func dot(with weightedPoint: PointWeighted) -> UIBezierPath { + let path = UIBezierPath() + path.addArc(withCenter: weightedPoint.point, radius: weightedPoint.weight / 2.0, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true) + + return path } - - struct Point { - let position: CGPoint - let width: CGFloat - - init(position: CGPoint, width: CGFloat) { - self.position = position - self.width = width - } - } - - private(set) var points: [Point] = [] - private var smoothPoints: [Point] = [] - private var segments: [Segment] = [] - private var lastWidth: CGFloat? - - private let minLineWidth: CGFloat - let lineWidth: CGFloat - - let color: UIColor - - init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) { - self.color = color - self.minLineWidth = minLineWidth - self.lineWidth = lineWidth - } - - func draw(at point: Polyline.Point) -> CGRect { - let width = extractLineWidth(from: point.velocity) - self.lastWidth = width - - let point = Point(position: point.location, width: width) - return appendPoint(point) - } - - func drawInContext(_ context: CGContext) { - self.drawSegments(self.segments, inContext: context) - } - - func extractLineWidth(from velocity: CGFloat) -> CGFloat { - let minValue = self.minLineWidth - let maxValue = self.lineWidth - - var size = max(minValue, min(maxValue + 1 - (velocity / 150), maxValue)) - if let lastWidth = self.lastWidth { - size = size * 0.2 + lastWidth * 0.8 - } - return size - } - - func appendPoint(_ point: Point) -> CGRect { - self.points.append(point) - - guard self.points.count > 2 else { return .null } - - let index = self.points.count - 1 - let point0 = self.points[index - 2] - let point1 = self.points[index - 1] - let point2 = self.points[index] - - let newSmoothPoints = smoothPoints( - fromPoint0: point0, - point1: point1, - point2: point2 - ) - - let lastOldSmoothPoint = smoothPoints.last - smoothPoints.append(contentsOf: newSmoothPoints) - - guard smoothPoints.count > 1 else { return .null } - - let newSegments: ([Segment], CGRect) = { - guard let lastOldSmoothPoint = lastOldSmoothPoint else { - return segments(fromSmoothPoints: newSmoothPoints) - } - return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints) - }() - segments.append(contentsOf: newSegments.0) - - return newSegments.1 - } - - func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] { - var smoothPoints = [Point]() - - let midPoint1 = (point0.position + point1.position) * 0.5 - let midPoint2 = (point1.position + point2.position) * 0.5 - - let segmentDistance = 2.0 - let distance = midPoint1.distance(to: midPoint2) - let numberOfSegments = min(128, max(floor(distance/segmentDistance), 32)) - - let step = 1.0 / numberOfSegments - for t in stride(from: 0, to: 1, by: step) { - let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t - let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5) - let point = Point(position: position, width: size) - smoothPoints.append(point) - } - - let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5) - smoothPoints.append(finalPoint) - - return smoothPoints - } - - func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) { - var segments = [Segment]() - var updateRect = CGRect.null - for i in 1 ..< smoothPoints.count { - let previousPoint = smoothPoints[i - 1].position - let previousWidth = smoothPoints[i - 1].width - let currentPoint = smoothPoints[i].position - let currentWidth = smoothPoints[i].width - let direction = currentPoint - previousPoint - - guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else { - continue - } - - var perpendicular = CGPoint(x: -direction.y, y: direction.x) - let length = perpendicular.length - if length > 0.0 { - perpendicular = perpendicular / length - } - - let a = previousPoint + perpendicular * previousWidth / 2 - let b = previousPoint - perpendicular * previousWidth / 2 - let c = currentPoint + perpendicular * currentWidth / 2 - let d = currentPoint - perpendicular * currentWidth / 2 - - let ab: CGPoint = { - let center = (b + a)/2 - let radius = center - b - return .init(x: center.x - radius.y, y: center.y + radius.x) - }() - let cd: CGPoint = { - let center = (c + d)/2 - let radius = center - c - return .init(x: center.x + radius.y, y: center.y - radius.x) - }() - - let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x) - let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y) - let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x) - let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y) - - updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)) - - segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth)) - } - return (segments, updateRect) - } - - func drawSegments(_ segments: [Segment], inContext context: CGContext) { - for segment in segments { - context.beginPath() - context.setStrokeColor(color.cgColor) - context.setFillColor(color.cgColor) - - context.move(to: segment.b) - - let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x) - context.addArc( - center: (segment.a + segment.b)/2, - radius: segment.abWidth/2, - startAngle: abStartAngle, - endAngle: abStartAngle + .pi, - clockwise: true - ) - context.addLine(to: segment.c) - - let cdStartAngle = atan2(segment.c.y - segment.d.y, segment.c.x - segment.d.x) - context.addArc( - center: (segment.c + segment.d)/2, - radius: segment.cdWidth/2, - startAngle: cdStartAngle, - endAngle: cdStartAngle + .pi, - clockwise: true - ) - context.closePath() - - context.fillPath() - context.strokePath() + class func curve(withPointA pointA: PointWeighted, pointB: PointWeighted) -> UIBezierPath { + let lines = normalToLine(from: pointA, to: pointB) + + let path = UIBezierPath() + path.move(to: lines.0.start) + path.addLine(to: lines.1.start) + let arcA = lines.1.start + let arcB = lines.1.end + path.addQuadCurve(to: arcB, controlPoint: pointA.point.forward(to: pointB.point, by: arcA.distance(to: arcB) / 1.1)) + path.addLine(to: lines.0.end) + path.close() + + return path + } + + class func curve(withPointA pointA: PointWeighted, pointB: PointWeighted, pointC: PointWeighted) -> UIBezierPath { + let linesAB = normalToLine(from: pointA, to: pointB) + let linesBC = normalToLine(from: pointB, to: pointC) + + let lineA = linesAB.0 + let lineB = linesAB.1.average(with: linesBC.0) + let lineC = linesBC.1 + + let path = UIBezierPath() + path.move(to: lineA.start) + path.addQuadCurve(to: lineC.start, controlPoint: lineB.start) + let arcA = lineC.start + let arcB = lineC.end + + path.addQuadCurve(to: arcB, controlPoint: pointB.point.forward(to: pointC.point, by: arcA.distance(to: arcB) / 1.1)) + path.addQuadCurve(to: lineA.end, controlPoint: lineB.end) + path.close() + + return path + } + + class func line(withPointA pointA: PointWeighted, pointB: PointWeighted, pointC: PointWeighted, prevLineSegment: LineSegment, roundedEnd: Bool = true) -> (UIBezierPath, LineSegment) { + let linesAB = normalToLine(from: pointA, to: pointB) + let linesBC = normalToLine(from: pointB, to: pointC) + +// let lineA = linesAB.0 + let lineB = linesAB.1.average(with: linesBC.0) + let lineC = linesBC.1 + + let path = UIBezierPath() + path.move(to: prevLineSegment.start) + path.addQuadCurve(to: lineC.start, controlPoint: lineB.start) + if roundedEnd { + let arcA = lineC.start + let arcB = lineC.end + + path.addQuadCurve(to: arcB, controlPoint: pointB.point.forward(to: pointC.point, by: arcA.distance(to: arcB) / 1.1)) + } else { + path.addLine(to: lineC.end) } + path.addQuadCurve(to: prevLineSegment.end, controlPoint: lineB.end) + path.close() + + return (path, lineC) + } + + class func curve(withPointA pointA: PointWeighted, pointB: PointWeighted, pointC: PointWeighted, pointD: PointWeighted) -> UIBezierPath { + let linesAB = normalToLine(from: pointA, to: pointB) + let linesBC = normalToLine(from: pointB, to: pointC) + let linesCD = normalToLine(from: pointC, to: pointD) + + let lineA = linesAB.0 + let lineB = linesAB.1.average(with: linesBC.0) + let lineC = linesBC.1.average(with: linesCD.0) + let lineD = linesCD.1 + + let path = UIBezierPath() + path.move(to: lineA.start) + path.addCurve(to: lineD.start, controlPoint1: lineB.start, controlPoint2: lineC.start) + let arcA = lineD.start + let arcB = lineD.end + path.addQuadCurve(to: arcB, controlPoint: pointC.point.forward(to: pointD.point, by: arcA.distance(to: arcB) / 1.1)) + path.addCurve(to: lineA.end, controlPoint1: lineC.end, controlPoint2: lineB.end) + path.close() + + return path + } + + class func normalToLine(from pointA: PointWeighted, to pointB: PointWeighted) -> (LineSegment, LineSegment) { + let line = LineSegment(start: pointA.point, end: pointB.point) + + return (line.normalLine(from: pointA), line.normalLine(from: pointB)) } } + + +// +//final class PenTool: DrawingElement { +// class RenderLayer: SimpleLayer, DrawingRenderLayer { +// func setup(size: CGSize) { +// self.shouldRasterize = true +// self.contentsScale = 1.0 +// +// let bounds = CGRect(origin: .zero, size: size) +// self.frame = bounds +// } +// +// private var line: StrokeLine? +// fileprivate func draw(line: StrokeLine, rect: CGRect) { +// self.line = line +// self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0)) +// } +// +// override func draw(in ctx: CGContext) { +// self.line?.drawInContext(ctx) +// } +// } +// +// let uuid = UUID() +// +// let drawingSize: CGSize +// let color: DrawingColor +// let lineWidth: CGFloat +// let arrow: Bool +// +// var path: Polyline? +// var boundingBox: CGRect? +// +// private var renderLine: StrokeLine +// var didSetupArrow = false +// private var renderLineArrow1: StrokeLine? +// private var renderLineArrow2: StrokeLine? +// let renderLineWidth: CGFloat +// +// var translation = CGPoint() +// +// private var currentRenderLayer: DrawingRenderLayer? +// +// var bounds: CGRect { +// return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero +// } +// +// var points: [Polyline.Point] { +// guard let linePath = self.path else { +// return [] +// } +// var points: [Polyline.Point] = [] +// for point in linePath.points { +// points.append(point.offsetBy(self.translation)) +// } +// return points +// } +// +// func containsPoint(_ point: CGPoint) -> Bool { +// return false +// // return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false +// } +// +// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool { +// if let linePath = self.path { +// let pathBoundingBox = path.bounds +// if self.bounds.intersects(pathBoundingBox) { +// for point in linePath.points { +// if path.contains(point.location.offsetBy(self.translation)) { +// return true +// } +// } +// } +// } +// return false +// } +// +// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) { +// self.drawingSize = drawingSize +// self.color = color +// self.lineWidth = lineWidth +// self.arrow = arrow +// +// let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003) +// let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09) +// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth +// +// self.renderLineWidth = lineWidth +// +// self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth) +// if arrow { +// self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) +// self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8) +// } +// } +// +// func setupRenderLayer() -> DrawingRenderLayer? { +// let layer = RenderLayer() +// layer.setup(size: self.drawingSize) +// self.currentRenderLayer = layer +// return layer +// } +// +// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) { +// guard case let .polyline(line) = path, let point = line.points.last else { +// return +// } +// self.path = line +// +// let rect = self.renderLine.draw(at: point) +// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { +// currentRenderLayer.draw(line: self.renderLine, rect: rect) +// } +// // self.path = bezierPath +// +// // if self.arrow && polyline.isComplete, polyline.points.count > 2 { +// // let lastPoint = lastPosition +// // var secondPoint = polyline.points[polyline.points.count - 2] +// // if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth { +// // secondPoint = polyline.points[polyline.points.count - 3] +// // } +// // let angle = lastPoint.angle(to: secondPoint.location) +// // let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15) +// // let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15) +// // +// // let arrowPath = UIBezierPath() +// // arrowPath.move(to: point2) +// // arrowPath.addLine(to: lastPoint) +// // arrowPath.addLine(to: point1) +// // let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) +// // +// // combinedPath.usesEvenOddFillRule = false +// // combinedPath.append(UIBezierPath(cgPath: arrowThickPath)) +// // } +// +// // let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) +// // self.renderPath = cgPath +// +// // if let currentRenderLayer = self.currentRenderLayer as? RenderLayer { +// // currentRenderLayer.updatePath(cgPath) +// // } +// } +// +// func draw(in context: CGContext, size: CGSize) { +// context.saveGState() +// +// context.translateBy(x: self.translation.x, y: self.translation.y) +// +// context.setShouldAntialias(true) +// +// if self.arrow, let path = self.path, let lastPoint = path.points.last { +// var lastPointWithVelocity: Polyline.Point? +// for point in path.points.reversed() { +// if point.velocity > 0.0 { +// lastPointWithVelocity = point +// break +// } +// } +// if !self.didSetupArrow, let lastPointWithVelocity = lastPointWithVelocity { +// let w = self.renderLineWidth +// var dist: CGFloat = 18.0 * sqrt(w) +// let spread: CGFloat = .pi * max(0.05, 0.03 * sqrt(w)) +// +// let suffix = path.points.suffix(100).reversed() +// +// var p0 = suffix.first! +// +// var p2 = suffix.last! +// var d: CGFloat = 0 +// for p in suffix { +// d += hypot(p0.location.x - p.location.x, p0.location.y - p.location.y) +// if d >= dist { +// p2 = p +// break +// } +// p0 = p +// } +// +// p0 = suffix.first! +// dist = min(dist, hypot(p0.location.x - p2.location.x, p0.location.y - p2.location.y)) +// +// var i = 0 +// for spread in [-spread, spread] { +// var points: [CGPoint] = [] +// points.append(lastPoint.location) +// +// p0 = suffix.first! +// var prev = p0.location +// d = 0 +// for p in suffix { +// let d1 = hypot(p0.location.x - p.location.x, p0.location.y - p.location.y) +// d += d1 +// if d >= dist { +// break +// } +// let d2 = d1 / cos(spread) +// let angle = atan2(p.location.y - p0.location.y, p.location.x - p0.location.x) +// let cur = CGPoint(x: prev.x + d2 * cos(angle + spread), y: prev.y + d2 * sin(angle + spread)) +// +// points.append( +// cur +// ) +// +// p0 = p +// prev = cur +// } +// +// for point in points { +// if i == 0 { +// let _ = self.renderLineArrow1?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint)) +// } else if i == 1 { +// let _ = self.renderLineArrow2?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint)) +// } +// } +// i += 1 +// } +// self.didSetupArrow = true +// } +// self.renderLineArrow1?.drawInContext(context) +// self.renderLineArrow2?.drawInContext(context) +// } +// +// self.renderLine.drawInContext(context) +// +// context.restoreGState() +// } +//} +// +//private class StrokeLine { +// struct Segment { +// let a: CGPoint +// let b: CGPoint +// let c: CGPoint +// let d: CGPoint +// let abWidth: CGFloat +// let cdWidth: CGFloat +// } +// +// struct Point { +// let position: CGPoint +// let width: CGFloat +// +// init(position: CGPoint, width: CGFloat) { +// self.position = position +// self.width = width +// } +// } +// +// private(set) var points: [Point] = [] +// private var smoothPoints: [Point] = [] +// private var segments: [Segment] = [] +// private var lastWidth: CGFloat? +// +// private let minLineWidth: CGFloat +// let lineWidth: CGFloat +// +// let color: UIColor +// +// init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) { +// self.color = color +// self.minLineWidth = minLineWidth +// self.lineWidth = lineWidth +// } +// +// func draw(at point: Polyline.Point) -> CGRect { +// let width = extractLineWidth(from: point.velocity) +// self.lastWidth = width +// +// let point = Point(position: point.location, width: width) +// return appendPoint(point) +// } +// +// func drawInContext(_ context: CGContext) { +// self.drawSegments(self.segments, inContext: context) +// } +// +// func extractLineWidth(from velocity: CGFloat) -> CGFloat { +// let minValue = self.minLineWidth +// let maxValue = self.lineWidth +// +// var size = max(minValue, min(maxValue + 1 - (velocity / 150), maxValue)) +// if let lastWidth = self.lastWidth { +// size = size * 0.2 + lastWidth * 0.8 +// } +// return size +// } +// +// func appendPoint(_ point: Point) -> CGRect { +// self.points.append(point) +// +// guard self.points.count > 2 else { return .null } +// +// let index = self.points.count - 1 +// let point0 = self.points[index - 2] +// let point1 = self.points[index - 1] +// let point2 = self.points[index] +// +// let newSmoothPoints = smoothPoints( +// fromPoint0: point0, +// point1: point1, +// point2: point2 +// ) +// +// let lastOldSmoothPoint = smoothPoints.last +// smoothPoints.append(contentsOf: newSmoothPoints) +// +// guard smoothPoints.count > 1 else { return .null } +// +// let newSegments: ([Segment], CGRect) = { +// guard let lastOldSmoothPoint = lastOldSmoothPoint else { +// return segments(fromSmoothPoints: newSmoothPoints) +// } +// return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints) +// }() +// segments.append(contentsOf: newSegments.0) +// +// return newSegments.1 +// } +// +// func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] { +// var smoothPoints = [Point]() +// +// let midPoint1 = (point0.position + point1.position) * 0.5 +// let midPoint2 = (point1.position + point2.position) * 0.5 +// +// let segmentDistance = 2.0 +// let distance = midPoint1.distance(to: midPoint2) +// let numberOfSegments = min(128, max(floor(distance/segmentDistance), 32)) +// +// let step = 1.0 / numberOfSegments +// for t in stride(from: 0, to: 1, by: step) { +// let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t +// let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5) +// let point = Point(position: position, width: size) +// smoothPoints.append(point) +// } +// +// let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5) +// smoothPoints.append(finalPoint) +// +// return smoothPoints +// } +// +// func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) { +// var segments = [Segment]() +// var updateRect = CGRect.null +// for i in 1 ..< smoothPoints.count { +// let previousPoint = smoothPoints[i - 1].position +// let previousWidth = smoothPoints[i - 1].width +// let currentPoint = smoothPoints[i].position +// let currentWidth = smoothPoints[i].width +// let direction = currentPoint - previousPoint +// +// guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else { +// continue +// } +// +// var perpendicular = CGPoint(x: -direction.y, y: direction.x) +// let length = perpendicular.length +// if length > 0.0 { +// perpendicular = perpendicular / length +// } +// +// let a = previousPoint + perpendicular * previousWidth / 2 +// let b = previousPoint - perpendicular * previousWidth / 2 +// let c = currentPoint + perpendicular * currentWidth / 2 +// let d = currentPoint - perpendicular * currentWidth / 2 +// +// let ab: CGPoint = { +// let center = (b + a)/2 +// let radius = center - b +// return .init(x: center.x - radius.y, y: center.y + radius.x) +// }() +// let cd: CGPoint = { +// let center = (c + d)/2 +// let radius = center - c +// return .init(x: center.x + radius.y, y: center.y - radius.x) +// }() +// +// let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x) +// let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y) +// let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x) +// let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y) +// +// updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)) +// +// segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth)) +// } +// return (segments, updateRect) +// } +// +// func drawSegments(_ segments: [Segment], inContext context: CGContext) { +// for segment in segments { +// context.beginPath() +// +// context.setStrokeColor(color.cgColor) +// context.setFillColor(color.cgColor) +// +// context.move(to: segment.b) +// +// let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x) +// context.addArc( +// center: (segment.a + segment.b)/2, +// radius: segment.abWidth/2, +// startAngle: abStartAngle, +// endAngle: abStartAngle + .pi, +// clockwise: true +// ) +// context.addLine(to: segment.c) +// +// let cdStartAngle = atan2(segment.c.y - segment.d.y, segment.c.x - segment.d.x) +// context.addArc( +// center: (segment.c + segment.d)/2, +// radius: segment.cdWidth/2, +// startAngle: cdStartAngle, +// endAngle: cdStartAngle + .pi, +// clockwise: true +// ) +// context.closePath() +// +// context.fillPath() +// context.strokePath() +// } +// } +//} +// diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index aff78e071a..b9ccb4f813 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -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,7 +226,7 @@ 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() diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index 4968785bae..cd6ef24d29 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -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,35 @@ 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) { self.currentParams = (size, color) self.emitterNode.frame = CGRect(origin: CGPoint(), size: size) + self.emitterMaskNode.frame = self.emitterNode.bounds + self.emitterMaskFillNode.frame = self.emitterNode.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 } } } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index 7e0cd2563e..afead08bf9 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -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) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index 1975a39568..bcf864bbd2 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -86,6 +86,12 @@ - (void)setTimer:(NSNumber *)timer forItem:(NSObject *)item; - (SSignal *)timersUpdatedSignal; +- (bool)spoilerForItem:(NSObject *)item; +- (SSignal *)spoilerSignalForItem:(NSObject *)item; +- (SSignal *)spoilerSignalForIdentifier:(NSString *)identifier; +- (void)setSpoiler:(bool)spoiler forItem:(NSObject *)item; +- (SSignal *)spoilersUpdatedSignal; + - (UIImage *)paintingImageForItem:(NSObject *)item; - (UIImage *)stillPaintingImageForItem:(NSObject *)item; - (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)image forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h index f1012f7edd..624ff74ea8 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h @@ -2,7 +2,7 @@ @interface TGPhotoVideoEditor : NSObject -+ (void)presentWithContext:(id)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)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed; + (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed; diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 1b933d7183..d09dec21e7 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -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]); } diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 726fe4b36e..40e90e8bf5 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -54,6 +54,16 @@ @end +@interface TGMediaSpoilerUpdate : NSObject + +@property (nonatomic, readonly, strong) id item; +@property (nonatomic, readonly) bool spoiler; + ++ (instancetype)spoilerUpdateWithItem:(id)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 *)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 *)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 *)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)item synchronous:(bool)synchronous { NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; @@ -1082,3 +1164,23 @@ } @end + + +@implementation TGMediaSpoilerUpdate + ++ (instancetype)spoilerUpdateWithItem:(id)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 diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index d87e86069e..55f00986cf 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -50,7 +50,7 @@ UIView *parentView = [self _parentView]; id inputPanel = nil; - if (_stickersContext) { + if (_stickersContext && _stickersContext.captionPanelView != nil) { inputPanel = _stickersContext.captionPanelView(); } _inputPanel = inputPanel; diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 30e5288384..09600fb6a7 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -12,7 +12,7 @@ @implementation TGPhotoVideoEditor -+ (void)presentWithContext:(id)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)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed { id windowManager = [context makeOverlayWindowManager]; @@ -35,19 +35,23 @@ 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.stickersContext = stickersContext; controller.skipInitialTransition = true; controller.dontHideStatusBar = true; controller.didFinishEditing = ^(__unused id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges, void(^commit)(void)) { if (didFinishWithImage != nil) didFinishWithImage(resultImage); + + commit(); }; controller.didFinishEditingVideo = ^(AVAsset *asset, id 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 editableItem) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift index caaa997f72..2f72cb2f5e 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift @@ -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, 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, signup: Bool, t } } } + + +public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, 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, 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() + }) + }) +} diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 93158a7724..fb33839c68 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -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)) diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index f8d7de74c5..37dd56c6e6 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/SparseItemGrid:SparseItemGrid", "//submodules/UndoUI:UndoUI", "//submodules/MoreButtonNode:MoreButtonNode", + "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 8d9d91e3db..0e1cfff5cb 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -12,6 +12,7 @@ import Photos import CheckNode import LegacyComponents import PhotoResources +import InvisibleInkDustNode enum MediaPickerGridItemContent: Equatable { case asset(PHFetchResult, Int) @@ -87,6 +88,9 @@ final class MediaPickerGridItemNode: GridItemNode { private var interaction: MediaPickerInteraction? private var theme: PresentationTheme? + private let spoilerDisposable = MetaDisposable() + private var spoilerNode: SpoilerOverlayNode? + private var currentIsPreviewing = false var selected: (() -> Void)? @@ -113,6 +117,10 @@ final class MediaPickerGridItemNode: GridItemNode { self.addSubnode(self.imageNode) } + + deinit { + self.spoilerDisposable.dispose() + } var identifier: String { return self.selectableItem?.uniqueIdentifier ?? "" @@ -170,17 +178,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 +309,31 @@ final class MediaPickerGridItemNode: GridItemNode { } self.imageNode.setSignal(imageSignal) + let spoilerSignal = Signal { 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 +367,25 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } + private func updateHasSpoiler(_ hasSpoiler: Bool) { + if hasSpoiler { + if self.spoilerNode == nil { + let spoilerNode = SpoilerOverlayNode() + self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) + self.spoilerNode = spoilerNode + + 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 +400,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 +421,61 @@ final class MediaPickerGridItemNode: GridItemNode { } } +class SpoilerOverlayNode: ASDisplayNode { + private let blurNode: NavigationBackgroundNode + private let dustNode: MediaDustNode + + private var maskView: UIView? + private var maskLayer: CAShapeLayer? + + override init() { + self.blurNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.1), enableBlur: true) + + 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 update(size: CGSize, transition: ContainedViewLayoutTransition) { + self.blurNode.update(size: size, transition: transition) + self.blurNode.frame = CGRect(origin: .zero, size: size) + + self.dustNode.frame = CGRect(origin: .zero, size: size) + self.dustNode.update(size: size, color: .white) + +// var leftOffset: CGFloat = 0.0 +// var rightOffset: CGFloat = 0.0 +// let corners = corners ?? ImageCorners(radius: 16.0) +// if case .Tail = corners.bottomLeft { +// leftOffset = 4.0 +// } else if case .Tail = corners.bottomRight { +// rightOffset = 4.0 +// } +// let rect = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: CGSize(width: size.width - leftOffset - rightOffset, height: size.height)) +// let path = UIBezierPath(roundRect: rect, topLeftRadius: corners.topLeft.radius, topRightRadius: corners.topRight.radius, bottomLeftRadius: corners.bottomLeft.radius, bottomRightRadius: corners.bottomRight.radius) +// let buttonPath = UIBezierPath(roundedRect: self.buttonNode.frame, cornerRadius: 16.0) +// path.append(buttonPath) +// path.usesEvenOddFillRule = true +// self.maskLayer?.path = path.cgPath + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index a1b693acdb..cc59c4475e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -600,7 +600,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } if let node = node { return (node.view, { [weak node] animateCheckNode in - node?.animateFadeIn(animateCheckNode: animateCheckNode) + 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 = 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)) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 5b2cc83554..d53e10384e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -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,71 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.addSubnode(self.imageNode) - if asset.isVideo, let editingState = interaction?.editingState { - func adjustmentsChangedSignal(editingState: TGMediaEditingContext) -> Signal { - 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 { + 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 { 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) + })) } } deinit { self.adjustmentsDisposable?.dispose() + self.spoilerDisposable.dispose() } override func didLoad() { @@ -120,6 +151,25 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.interaction?.openSelectedMedia(asset, self.imageNode.image) } + private func updateHasSpoiler(_ hasSpoiler: Bool) { + if hasSpoiler { + if self.spoilerNode == nil { + let spoilerNode = SpoilerOverlayNode() + self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) + self.spoilerNode = spoilerNode + + 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 { subscriber in @@ -229,6 +279,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 +303,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)) diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 22d5fca5e4..9dacb156b0 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -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 } } +private 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, 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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift index 7d78414fbd..67fd8f180c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift @@ -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 } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 3b4a419a87..5e94dfc092 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index be0b8254e0..0c53822d39 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -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) diff --git a/submodules/TelegramUI/Resources/Animations/anim_spoiler.json b/submodules/TelegramUI/Resources/Animations/anim_spoiler.json new file mode 100644 index 0000000000..40fea5542f --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_spoiler.json @@ -0,0 +1 @@ +{"v":"5.9.6","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"spoiler","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"blurIcon Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2,"l":2},"a":{"a":0,"k":[12,12,0],"ix":1,"l":2},"s":{"a":0,"k":[2133.333,2133.333,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[20.5,15.83],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":24,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":36,"s":[120,120]},{"t":48,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[18.167,15.995],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":22,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":34,"s":[120,120]},{"t":46,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.552],[0.552,0],[0,0.552],[-0.552,0]],"o":[[0,0.552],[-0.552,0],[0,-0.552],[0.552,0]],"v":[[1,0],[0,1],[-1,0],[0,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[15.333,16.165],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":20,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":32,"s":[120,120]},{"t":44,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.643],[0.643,0],[0,0.643],[-0.643,0]],"o":[[0,0.643],[-0.643,0],[0,-0.643],[0.643,0]],"v":[[1.165,0],[0,1.165],[-1.165,0],[0,-1.165]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[12,16.165],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":18,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":30,"s":[120,120]},{"t":42,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.552],[0.552,0],[0,0.552],[-0.552,0]],"o":[[0,0.552],[-0.552,0],[0,-0.552],[0.552,0]],"v":[[1,0],[0,1],[-1,0],[0,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[8.667,16.165],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":16,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":28,"s":[120,120]},{"t":40,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[5.833,15.995],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":14,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":26,"s":[120,120]},{"t":38,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[3.5,15.83],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":12,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":24,"s":[120,120]},{"t":36,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[20.5,8.17],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":20,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":32,"s":[120,120]},{"t":44,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[18.167,7.835],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":18,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":30,"s":[120,120]},{"t":42,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":1,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.552],[0.552,0],[0,0.552],[-0.552,0]],"o":[[0,0.552],[-0.552,0],[0,-0.552],[0.552,0]],"v":[[1,0],[0,1],[-1,0],[0,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[15.333,7.835],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":16,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":28,"s":[120,120]},{"t":40,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":1,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.643],[0.643,0],[0,0.643],[-0.643,0]],"o":[[0,0.643],[-0.643,0],[0,-0.643],[0.643,0]],"v":[[1.165,0],[0,1.165],[-1.165,0],[0,-1.165]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[12,7.835],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":14,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":26,"s":[120,120]},{"t":38,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":1,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.552],[0.552,0],[0,0.552],[-0.552,0]],"o":[[0,0.552],[-0.552,0],[0,-0.552],[0.552,0]],"v":[[1,0],[0,1],[-1,0],[0,-1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[8.667,7.835],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":12,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":24,"s":[120,120]},{"t":36,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":1,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[5.833,7.835],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":10,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":22,"s":[120,120]},{"t":34,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":1,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[3.5,8.17],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":8,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":20,"s":[120,120]},{"t":32,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":1,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[20.335,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":22,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":34,"s":[120,120]},{"t":46,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":1,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.461],[0.461,0],[0,0.461],[-0.461,0]],"o":[[0,0.461],[-0.461,0],[0,-0.461],[0.461,0]],"v":[[0.835,0],[0,0.835],[-0.835,0],[0,-0.835]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[18.057,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":20,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":32,"s":[120,120]},{"t":44,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":1,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.643],[0.643,0],[0,0.643],[-0.643,0]],"o":[[0,0.643],[-0.643,0],[0,-0.643],[0.643,0]],"v":[[1.165,0],[0,1.165],[-1.165,0],[0,-1.165]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[15.278,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":18,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":30,"s":[120,120]},{"t":42,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 17","np":1,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.737],[0.737,0],[0,0.737],[-0.737,0]],"o":[[0,0.737],[-0.737,0],[0,-0.737],[0.737,0]],"v":[[1.335,0],[0,1.335],[-1.335,0],[0,-1.335]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[12,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":16,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":28,"s":[120,120]},{"t":40,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 18","np":1,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.643],[0.643,0],[0,0.643],[-0.643,0]],"o":[[0,0.643],[-0.643,0],[0,-0.643],[0.643,0]],"v":[[1.165,0],[0,1.165],[-1.165,0],[0,-1.165]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[8.722,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":14,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":26,"s":[120,120]},{"t":38,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 19","np":1,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.461],[0.461,0],[0,0.461],[-0.461,0]],"o":[[0,0.461],[-0.461,0],[0,-0.461],[0.461,0]],"v":[[0.835,0],[0,0.835],[-0.835,0],[0,-0.835]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[5.943,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":12,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":24,"s":[120,120]},{"t":36,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 20","np":1,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[3.665,12.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":10,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":22,"s":[120,120]},{"t":34,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 21","np":1,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.185],[0.185,0],[0,0.185],[-0.185,0]],"o":[[0,0.185],[-0.185,0],[0,-0.185],[0.185,0]],"v":[[0.335,0],[0,0.335],[-0.335,0],[0,-0.335]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[20.665,4.665],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":18,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":30,"s":[120,120]},{"t":42,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 22","np":1,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[18.108,4.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":16,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":28,"s":[120,120]},{"t":40,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 23","np":1,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[15.222,4.335],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":14,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":26,"s":[120,120]},{"t":38,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 24","np":1,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.461],[0.461,0],[0,0.461],[-0.461,0]],"o":[[0,0.461],[-0.461,0],[0,-0.461],[0.461,0]],"v":[[0.835,0],[0,0.835],[-0.835,0],[0,-0.835]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[12,4.165],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":6,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":12,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":24,"s":[120,120]},{"t":36,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 25","np":1,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[8.778,4.335],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":4,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":10,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":22,"s":[120,120]},{"t":34,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 26","np":1,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[5.892,4.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":2,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":8,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":20,"s":[120,120]},{"t":32,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 27","np":1,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.185],[0.185,0],[0,0.185],[-0.185,0]],"o":[[0,0.185],[-0.185,0],[0,-0.185],[0.185,0]],"v":[[0.335,0],[0,0.335],[-0.335,0],[0,-0.335]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[3.335,4.665],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":6,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":18,"s":[120,120]},{"t":30,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 28","np":1,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.185],[0.185,0],[0,0.185],[-0.185,0]],"o":[[0,0.185],[-0.185,0],[0,-0.185],[0.185,0]],"v":[[0.335,0],[0,0.335],[-0.335,0],[0,-0.335]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[20.665,19.335],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":26,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":38,"s":[120,120]},{"t":50,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 29","np":1,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[18.108,19.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":18,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":24,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":36,"s":[120,120]},{"t":48,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 30","np":1,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[15.222,19.665],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":22,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":34,"s":[120,120]},{"t":46,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 31","np":1,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.461],[0.461,0],[0,0.461],[-0.461,0]],"o":[[0,0.461],[-0.461,0],[0,-0.461],[0.461,0]],"v":[[0.835,0],[0,0.835],[-0.835,0],[0,-0.835]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[12,19.835],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":14,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":20,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":32,"s":[120,120]},{"t":44,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 32","np":1,"cix":2,"bm":0,"ix":32,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.367],[0.367,0],[0,0.367],[-0.367,0]],"o":[[0,0.367],[-0.367,0],[0,-0.367],[0.367,0]],"v":[[0.665,0],[0,0.665],[-0.665,0],[0,-0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[8.778,19.665],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":12,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":18,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":30,"s":[120,120]},{"t":42,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 33","np":1,"cix":2,"bm":0,"ix":33,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.276],[0.276,0],[0,0.276],[-0.276,0]],"o":[[0,0.276],[-0.276,0],[0,-0.276],[0.276,0]],"v":[[0.5,0],[0,0.5],[-0.5,0],[0,-0.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[5.892,19.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":16,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":28,"s":[120,120]},{"t":40,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 34","np":1,"cix":2,"bm":0,"ix":34,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.185],[0.185,0],[0,0.185],[-0.185,0]],"o":[[0,0.185],[-0.185,0],[0,-0.185],[0.185,0]],"v":[[0.335,0],[0,0.335],[-0.335,0],[0,-0.335]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[3.335,19.335],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":8,"s":[100,100]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":14,"s":[0,0]},{"i":{"x":[0.4,0.4],"y":[1,1]},"o":{"x":[0.6,0.6],"y":[0,0]},"t":26,"s":[120,120]},{"t":38,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 35","np":1,"cix":2,"bm":0,"ix":35,"mn":"ADBE Vector Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":184,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a1ab39e263..e2d8d5f482 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -820,6 +820,24 @@ 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 { + legacyAvatarEditor(context: strongSelf.context, media: .message(message: MessageReference(message), media: image), 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) + } + } + }) + } default: break } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 847fecda52..95deeb7290 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -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))) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 933f366330..bb59fc32c0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -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)?, imageFrame: CGRect, corners: ImageCorners?) { let spacing: CGFloat = 2.0 let padding: CGFloat = 10.0 + + if let (imageSignal, drawingSize) = imageSignal { + self.blurredImageNode.setSignal(imageSignal) + + let imageLayout = self.blurredImageNode.asyncLayout() + let arguments = TransformImageArguments(corners: corners ?? ImageCorners(), imageSize: drawingSize, boundingSize: imageFrame.size, 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.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)? 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,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let imageDimensions = imageDimensions { strongSelf.currentHighQualityImageSignal = (updateImageSignal(false, true), imageDimensions) + + if let updateBlurredImageSignal = updateBlurredImageSignal { + strongSelf.currentBlurredImageSignal = (updateBlurredImageSignal(false, true), imageDimensions) + } } } + + if let _ = secretBeginTimeAndTimeout { if updatedStatusSignal == nil, let fetchStatus = strongSelf.fetchStatus, case .Local = fetchStatus { if let statusNode = strongSelf.statusNode, case .secretTimeout = statusNode.state { @@ -1842,7 +1883,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 +1912,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 diff --git a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift index faa614c305..5cf99cccbd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -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,10 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode fatalError("init(coder:) has not been implemented") } + deinit { + self.fetchDisposable.dispose() + } + @objc private func buttonPressed() { guard let item = self.item else { return @@ -91,6 +102,7 @@ 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) @@ -98,24 +110,53 @@ class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode 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 isVideo = !(photo?.videoRepresentations.isEmpty ?? true) + let fromYou = item.message.author?.id == item.context.account.peerId - 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())) + 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 (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 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 { + 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 +168,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) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f296e0f869..52512980db 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -6741,11 +6741,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 +6757,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 +6813,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 +6829,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 +6972,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate })) } - private enum AvatarEditingMode { + fileprivate enum AvatarEditingMode { case generic case suggest case custom @@ -9317,6 +9325,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)