Merge commit 'b9dfb3d2a1a75d56f7a4e14c2109a821accbe677'

This commit is contained in:
Ali 2023-07-07 02:03:56 +04:00
commit 2657296945
31 changed files with 770 additions and 371 deletions

View File

@ -8,13 +8,17 @@ final class CameraSession {
private let singleSession: AVCaptureSession? private let singleSession: AVCaptureSession?
private let multiSession: Any? private let multiSession: Any?
let hasMultiCam: Bool
init() { init() {
if #available(iOS 13.0, *), AVCaptureMultiCamSession.isMultiCamSupported { if #available(iOS 13.0, *), AVCaptureMultiCamSession.isMultiCamSupported {
self.multiSession = AVCaptureMultiCamSession() self.multiSession = AVCaptureMultiCamSession()
self.singleSession = nil self.singleSession = nil
self.hasMultiCam = true
} else { } else {
self.singleSession = AVCaptureSession() self.singleSession = AVCaptureSession()
self.multiSession = nil self.multiSession = nil
self.hasMultiCam = false
} }
} }
@ -119,23 +123,9 @@ private final class CameraContext {
} }
} }
var previewView: CameraPreviewView? { var previewView: CameraPreviewView?
didSet {
}
}
var simplePreviewView: CameraSimplePreviewView? {
didSet {
if let oldValue {
Queue.mainQueue().async {
oldValue.invalidate()
self.simplePreviewView?.setSession(self.session.session, autoConnect: true)
}
}
}
}
var simplePreviewView: CameraSimplePreviewView?
var secondaryPreviewView: CameraSimplePreviewView? var secondaryPreviewView: CameraSimplePreviewView?
private var lastSnapshotTimestamp: Double = CACurrentMediaTime() private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
@ -161,7 +151,6 @@ private final class CameraContext {
} }
} }
private var videoOrientation: AVCaptureVideoOrientation?
init(queue: Queue, session: CameraSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?, secondaryPreviewView: CameraSimplePreviewView?) { init(queue: Queue, session: CameraSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?, secondaryPreviewView: CameraSimplePreviewView?) {
self.queue = queue self.queue = queue
self.session = session self.session = session
@ -371,7 +360,7 @@ private final class CameraContext {
self.previewNode?.enqueue(sampleBuffer) self.previewNode?.enqueue(sampleBuffer)
let timestamp = CACurrentMediaTime() let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5 { if timestamp > self.lastSnapshotTimestamp + 2.5, !self.mainDeviceContext.output.isRecording {
var mirror = false var mirror = false
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
mirror = connection.inputPorts.first?.sourceDevicePosition == .front mirror = connection.inputPorts.first?.sourceDevicePosition == .front
@ -450,7 +439,7 @@ private final class CameraContext {
} }
func takePhoto() -> Signal<PhotoCaptureResult, NoError> { func takePhoto() -> Signal<PhotoCaptureResult, NoError> {
let orientation = self.videoOrientation ?? .portrait let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait
if let additionalDeviceContext = self.additionalDeviceContext { if let additionalDeviceContext = self.additionalDeviceContext {
let dualPosition = self.positionValue let dualPosition = self.positionValue
return combineLatest( return combineLatest(
@ -565,9 +554,9 @@ public final class Camera {
session.session.automaticallyConfiguresApplicationAudioSession = false session.session.automaticallyConfiguresApplicationAudioSession = false
session.session.automaticallyConfiguresCaptureDeviceForWideColor = false session.session.automaticallyConfiguresCaptureDeviceForWideColor = false
if let previewView { if let previewView {
previewView.setSession(session.session, autoConnect: false) previewView.setSession(session.session, autoConnect: !session.hasMultiCam)
} }
if let secondaryPreviewView { if let secondaryPreviewView, session.hasMultiCam {
secondaryPreviewView.setSession(session.session, autoConnect: false) secondaryPreviewView.setSession(session.session, autoConnect: false)
} }

View File

@ -27,7 +27,11 @@ class CameraInput {
if let videoInput = try? AVCaptureDeviceInput(device: device) { if let videoInput = try? AVCaptureDeviceInput(device: device) {
self.videoInput = videoInput self.videoInput = videoInput
if session.session.canAddInput(videoInput) { if session.session.canAddInput(videoInput) {
session.session.addInputWithNoConnections(videoInput) if session.hasMultiCam {
session.session.addInputWithNoConnections(videoInput)
} else {
session.session.addInput(videoInput)
}
} }
} }
} }

View File

@ -118,7 +118,11 @@ final class CameraOutput: NSObject {
func configure(for session: CameraSession, device: CameraDevice, input: CameraInput, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) { func configure(for session: CameraSession, device: CameraDevice, input: CameraInput, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
if session.session.canAddOutput(self.videoOutput) { if session.session.canAddOutput(self.videoOutput) {
session.session.addOutputWithNoConnections(self.videoOutput) if session.hasMultiCam {
session.session.addOutputWithNoConnections(self.videoOutput)
} else {
session.session.addOutput(self.videoOutput)
}
self.videoOutput.setSampleBufferDelegate(self, queue: self.queue) self.videoOutput.setSampleBufferDelegate(self, queue: self.queue)
} }
if audio, session.session.canAddOutput(self.audioOutput) { if audio, session.session.canAddOutput(self.audioOutput) {
@ -126,7 +130,11 @@ final class CameraOutput: NSObject {
self.audioOutput.setSampleBufferDelegate(self, queue: self.queue) self.audioOutput.setSampleBufferDelegate(self, queue: self.queue)
} }
if photo, session.session.canAddOutput(self.photoOutput) { if photo, session.session.canAddOutput(self.photoOutput) {
session.session.addOutputWithNoConnections(self.photoOutput) if session.hasMultiCam {
session.session.addOutputWithNoConnections(self.photoOutput)
} else {
session.session.addOutput(self.photoOutput)
}
} }
if metadata, session.session.canAddOutput(self.metadataOutput) { if metadata, session.session.canAddOutput(self.metadataOutput) {
session.session.addOutput(self.metadataOutput) session.session.addOutput(self.metadataOutput)
@ -137,7 +145,7 @@ final class CameraOutput: NSObject {
} }
} }
if #available(iOS 13.0, *) { if #available(iOS 13.0, *), session.hasMultiCam {
if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) { if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) {
if let previewView { if let previewView {
let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: previewView.videoPreviewLayer) let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: previewView.videoPreviewLayer)
@ -163,57 +171,7 @@ final class CameraOutput: NSObject {
} }
} }
} }
func reconfigure(for session: CameraSession, device: CameraDevice, input: CameraInput, otherPreviewView: CameraSimplePreviewView?, otherOutput: CameraOutput) {
if #available(iOS 13.0, *) {
if let previewConnection = self.previewConnection {
if session.session.connections.contains(where: { $0 === previewConnection }) {
session.session.removeConnection(previewConnection)
}
self.previewConnection = nil
}
if let videoConnection = self.videoConnection {
if session.session.connections.contains(where: { $0 === videoConnection }) {
session.session.removeConnection(videoConnection)
}
self.videoConnection = nil
}
if let photoConnection = self.photoConnection {
if session.session.connections.contains(where: { $0 === photoConnection }) {
session.session.removeConnection(photoConnection)
}
self.photoConnection = nil
}
if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) {
if let otherPreviewView {
let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: otherPreviewView.videoPreviewLayer)
if session.session.canAddConnection(previewConnection) {
session.session.addConnection(previewConnection)
self.previewConnection = previewConnection
}
}
let videoConnection = AVCaptureConnection(inputPorts: ports, output: otherOutput.videoOutput)
if session.session.canAddConnection(videoConnection) {
session.session.addConnection(videoConnection)
self.videoConnection = videoConnection
}
let photoConnection = AVCaptureConnection(inputPorts: ports, output: otherOutput.photoOutput)
if session.session.canAddConnection(photoConnection) {
session.session.addConnection(photoConnection)
self.photoConnection = photoConnection
}
}
}
}
func toggleConnection() {
}
func invalidate(for session: CameraSession) { func invalidate(for session: CameraSession) {
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
if let previewConnection = self.previewConnection { if let previewConnection = self.previewConnection {
@ -314,6 +272,10 @@ final class CameraOutput: NSObject {
} }
} }
var isRecording: Bool {
return self.videoRecorder != nil
}
private var recordingCompletionPipe = ValuePipe<VideoCaptureResult>() private var recordingCompletionPipe = ValuePipe<VideoCaptureResult>()
func startRecording(isDualCamera: Bool, position: Camera.Position? = nil) -> Signal<Double, NoError> { func startRecording(isDualCamera: Bool, position: Camera.Position? = nil) -> Signal<Double, NoError> {
guard self.videoRecorder == nil else { guard self.videoRecorder == nil else {

View File

@ -32,8 +32,7 @@ public class CameraSimplePreviewView: UIView {
} else { } else {
statusBarOrientation = UIApplication.shared.statusBarOrientation statusBarOrientation = UIApplication.shared.statusBarOrientation
} }
let videoOrientation: AVCaptureVideoOrientation = statusBarOrientation.videoOrientation let videoOrientation = statusBarOrientation.videoOrientation
// videoPreviewLayer.frame = view.layer.bounds
self.videoPreviewLayer.connection?.videoOrientation = videoOrientation self.videoPreviewLayer.connection?.videoOrientation = videoOrientation
self.videoPreviewLayer.removeAllAnimations() self.videoPreviewLayer.removeAllAnimations()
} }
@ -78,7 +77,7 @@ public class CameraSimplePreviewView: UIView {
self.videoPreviewLayer.videoGravity = main ? .resizeAspectFill : .resizeAspect self.videoPreviewLayer.videoGravity = main ? .resizeAspectFill : .resizeAspect
self.placeholderView.contentMode = .scaleAspectFill self.placeholderView.contentMode = main ? .scaleAspectFill : .scaleAspectFit
self.addSubview(self.placeholderView) self.addSubview(self.placeholderView)
if main { if main {

View File

@ -201,93 +201,157 @@ func exifOrientationForDeviceOrientation(_ deviceOrientation: UIDeviceOrientatio
} }
} }
/**
First crops the pixel buffer, then resizes it.
This function requires the caller to pass in both the source and destination
pixel buffers. The dimensions of destination pixel buffer should be at least
`scaleWidth` x `scaleHeight` pixels.
*/
func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer, func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer,
to dstPixelBuffer: CVPixelBuffer, to dstPixelBuffer: CVPixelBuffer,
cropX: Int, cropX: Int,
cropY: Int, cropY: Int,
cropWidth: Int, cropWidth: Int,
cropHeight: Int, cropHeight: Int,
scaleWidth: Int, scaleWidth: Int,
scaleHeight: Int) { scaleHeight: Int) {
assert(CVPixelBufferGetWidth(dstPixelBuffer) >= scaleWidth) assert(CVPixelBufferGetWidth(dstPixelBuffer) >= scaleWidth)
assert(CVPixelBufferGetHeight(dstPixelBuffer) >= scaleHeight) assert(CVPixelBufferGetHeight(dstPixelBuffer) >= scaleHeight)
let srcFlags = CVPixelBufferLockFlags.readOnly let srcFlags = CVPixelBufferLockFlags.readOnly
let dstFlags = CVPixelBufferLockFlags(rawValue: 0) let dstFlags = CVPixelBufferLockFlags(rawValue: 0)
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(srcPixelBuffer, srcFlags) else { guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(srcPixelBuffer, srcFlags) else {
print("Error: could not lock source pixel buffer") print("Error: could not lock source pixel buffer")
return return
} }
defer { CVPixelBufferUnlockBaseAddress(srcPixelBuffer, srcFlags) } defer { CVPixelBufferUnlockBaseAddress(srcPixelBuffer, srcFlags) }
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(dstPixelBuffer, dstFlags) else { guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(dstPixelBuffer, dstFlags) else {
print("Error: could not lock destination pixel buffer") print("Error: could not lock destination pixel buffer")
return return
} }
defer { CVPixelBufferUnlockBaseAddress(dstPixelBuffer, dstFlags) } defer { CVPixelBufferUnlockBaseAddress(dstPixelBuffer, dstFlags) }
guard let srcData = CVPixelBufferGetBaseAddress(srcPixelBuffer), guard let srcData = CVPixelBufferGetBaseAddress(srcPixelBuffer),
let dstData = CVPixelBufferGetBaseAddress(dstPixelBuffer) else { let dstData = CVPixelBufferGetBaseAddress(dstPixelBuffer) else {
print("Error: could not get pixel buffer base address") print("Error: could not get pixel buffer base address")
return return
} }
let srcBytesPerRow = CVPixelBufferGetBytesPerRow(srcPixelBuffer) let srcBytesPerRow = CVPixelBufferGetBytesPerRow(srcPixelBuffer)
let offset = cropY*srcBytesPerRow + cropX*4 let offset = cropY*srcBytesPerRow + cropX*4
var srcBuffer = vImage_Buffer(data: srcData.advanced(by: offset), var srcBuffer = vImage_Buffer(data: srcData.advanced(by: offset),
height: vImagePixelCount(cropHeight), height: vImagePixelCount(cropHeight),
width: vImagePixelCount(cropWidth), width: vImagePixelCount(cropWidth),
rowBytes: srcBytesPerRow) rowBytes: srcBytesPerRow)
let dstBytesPerRow = CVPixelBufferGetBytesPerRow(dstPixelBuffer) let dstBytesPerRow = CVPixelBufferGetBytesPerRow(dstPixelBuffer)
var dstBuffer = vImage_Buffer(data: dstData, var dstBuffer = vImage_Buffer(data: dstData,
height: vImagePixelCount(scaleHeight), height: vImagePixelCount(scaleHeight),
width: vImagePixelCount(scaleWidth), width: vImagePixelCount(scaleWidth),
rowBytes: dstBytesPerRow) rowBytes: dstBytesPerRow)
let error = vImageScale_ARGB8888(&srcBuffer, &dstBuffer, nil, vImage_Flags(0)) let error = vImageScale_ARGB8888(&srcBuffer, &dstBuffer, nil, vImage_Flags(0))
if error != kvImageNoError { if error != kvImageNoError {
print("Error:", error) print("Error:", error)
} }
} }
/**
Resizes a CVPixelBuffer to a new width and height.
This function requires the caller to pass in both the source and destination
pixel buffers. The dimensions of destination pixel buffer should be at least
`width` x `height` pixels.
*/
func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer, func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer,
to dstPixelBuffer: CVPixelBuffer, to dstPixelBuffer: CVPixelBuffer,
width: Int, height: Int) { width: Int, height: Int) {
resizePixelBuffer(from: srcPixelBuffer, to: dstPixelBuffer, resizePixelBuffer(from: srcPixelBuffer, to: dstPixelBuffer,
cropX: 0, cropY: 0, cropX: 0, cropY: 0,
cropWidth: CVPixelBufferGetWidth(srcPixelBuffer), cropWidth: CVPixelBufferGetWidth(srcPixelBuffer),
cropHeight: CVPixelBufferGetHeight(srcPixelBuffer), cropHeight: CVPixelBufferGetHeight(srcPixelBuffer),
scaleWidth: width, scaleHeight: height) scaleWidth: width, scaleHeight: height)
} }
/**
Resizes a CVPixelBuffer to a new width and height, using Core Image.
*/
func resizePixelBuffer(_ pixelBuffer: CVPixelBuffer, func resizePixelBuffer(_ pixelBuffer: CVPixelBuffer,
width: Int, height: Int, width: Int, height: Int,
output: CVPixelBuffer, context: CIContext) { output: CVPixelBuffer, context: CIContext) {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let sx = CGFloat(width) / CGFloat(CVPixelBufferGetWidth(pixelBuffer)) let sx = CGFloat(width) / CGFloat(CVPixelBufferGetWidth(pixelBuffer))
let sy = CGFloat(height) / CGFloat(CVPixelBufferGetHeight(pixelBuffer)) let sy = CGFloat(height) / CGFloat(CVPixelBufferGetHeight(pixelBuffer))
let scaleTransform = CGAffineTransform(scaleX: sx, y: sy) let scaleTransform = CGAffineTransform(scaleX: sx, y: sy)
let scaledImage = ciImage.transformed(by: scaleTransform) let scaledImage = ciImage.transformed(by: scaleTransform)
context.render(scaledImage, to: output) context.render(scaledImage, to: output)
}
func imageFromCVPixelBuffer(_ pixelBuffer: CVPixelBuffer, orientation: UIImage.Orientation) -> UIImage? {
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(
data: baseAddress,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
) else {
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return nil
}
guard let cgImage = context.makeImage() else {
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return nil
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
}
extension CVPixelBuffer {
func deepCopy() -> CVPixelBuffer? {
let width = CVPixelBufferGetWidth(self)
let height = CVPixelBufferGetHeight(self)
let format = CVPixelBufferGetPixelFormatType(self)
let attributes: [NSObject: AnyObject] = [
kCVPixelBufferCGImageCompatibilityKey: true as AnyObject,
kCVPixelBufferCGBitmapContextCompatibilityKey: true as AnyObject
]
var newPixelBuffer: CVPixelBuffer?
let status = CVPixelBufferCreate(
kCFAllocatorDefault,
width,
height,
format,
attributes as CFDictionary,
&newPixelBuffer
)
guard status == kCVReturnSuccess, let unwrappedPixelBuffer = newPixelBuffer else {
return nil
}
CVPixelBufferLockBaseAddress(self, .readOnly)
CVPixelBufferLockBaseAddress(unwrappedPixelBuffer, [])
guard let sourceBaseAddress = CVPixelBufferGetBaseAddress(self),
let destinationBaseAddress = CVPixelBufferGetBaseAddress(unwrappedPixelBuffer) else {
CVPixelBufferUnlockBaseAddress(self, .readOnly)
CVPixelBufferUnlockBaseAddress(unwrappedPixelBuffer, [])
return nil
}
let sourceBytesPerRow = CVPixelBufferGetBytesPerRow(self)
let destinationBytesPerRow = CVPixelBufferGetBytesPerRow(unwrappedPixelBuffer)
let imageSize = height * min(sourceBytesPerRow, destinationBytesPerRow)
memcpy(destinationBaseAddress, sourceBaseAddress, imageSize)
CVPixelBufferUnlockBaseAddress(self, .readOnly)
CVPixelBufferUnlockBaseAddress(unwrappedPixelBuffer, [])
return unwrappedPixelBuffer
}
} }

View File

@ -34,7 +34,7 @@ private final class VideoRecorderImpl {
private var videoInput: AVAssetWriterInput? private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput?
private let imageContext: CIContext private let imageContext = CIContext()
private var transitionImage: UIImage? private var transitionImage: UIImage?
private var savedTransitionImage = false private var savedTransitionImage = false
@ -66,7 +66,6 @@ private final class VideoRecorderImpl {
self.configuration = configuration self.configuration = configuration
self.videoTransform = videoTransform self.videoTransform = videoTransform
self.url = fileUrl self.url = fileUrl
self.imageContext = CIContext()
try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: url)
guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else { guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else {
@ -162,10 +161,16 @@ private final class VideoRecorderImpl {
} }
if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData { if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData {
if videoInput.append(sampleBuffer) {
self.lastVideoSampleTime = presentationTime
let startTime = self.recordingStartSampleTime
let duration = presentationTime - startTime
self._duration = duration
}
if !self.savedTransitionImage, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { if !self.savedTransitionImage, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
self.savedTransitionImage = true self.savedTransitionImage = true
Queue.concurrentBackgroundQueue().async {
Queue.concurrentDefaultQueue().async {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) { if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) {
self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
@ -175,14 +180,6 @@ private final class VideoRecorderImpl {
} }
} }
if videoInput.append(sampleBuffer) {
self.lastVideoSampleTime = presentationTime
let startTime = self.recordingStartSampleTime
let duration = presentationTime - startTime
self._duration = duration
} else {
print("error")
}
if !self.tryAppendingPendingAudioBuffers() { if !self.tryAppendingPendingAudioBuffers() {
self.transitionToFailedStatus(error: .generic) self.transitionToFailedStatus(error: .generic)
} }

View File

@ -157,6 +157,8 @@ public final class Button: Component {
super.init(frame: frame) super.init(frame: frame)
self.isExclusiveTouch = true
self.addSubview(self.contentView) self.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)

View File

@ -297,9 +297,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
if let shape = entity as? DrawingSimpleShapeEntity { if let shape = entity as? DrawingSimpleShapeEntity {
shape.position = center shape.position = center
shape.rotation = rotation
if setup { if setup {
shape.rotation = rotation
let size = self.newEntitySize() let size = self.newEntitySize()
shape.referenceDrawingSize = self.size shape.referenceDrawingSize = self.size
if shape.shapeType == .star { if shape.shapeType == .star {
@ -319,15 +318,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} }
} else if let sticker = entity as? DrawingStickerEntity { } else if let sticker = entity as? DrawingStickerEntity {
sticker.position = center sticker.position = center
sticker.rotation = rotation
if setup { if setup {
sticker.rotation = rotation
sticker.referenceDrawingSize = self.size sticker.referenceDrawingSize = self.size
sticker.scale = zoomScale sticker.scale = zoomScale
} }
} else if let bubble = entity as? DrawingBubbleEntity { } else if let bubble = entity as? DrawingBubbleEntity {
bubble.position = center bubble.position = center
bubble.rotation = rotation
if setup { if setup {
bubble.rotation = rotation
let size = self.newEntitySize() let size = self.newEntitySize()
bubble.referenceDrawingSize = self.size bubble.referenceDrawingSize = self.size
bubble.size = CGSize(width: size.width, height: round(size.height * 0.7)) bubble.size = CGSize(width: size.width, height: round(size.height * 0.7))
@ -335,8 +334,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} }
} else if let text = entity as? DrawingTextEntity { } else if let text = entity as? DrawingTextEntity {
text.position = center text.position = center
text.rotation = rotation
if setup { if setup {
text.rotation = rotation
text.referenceDrawingSize = self.size text.referenceDrawingSize = self.size
text.width = floor(self.size.width * 0.9) text.width = floor(self.size.width * 0.9)
text.fontSize = 0.08 text.fontSize = 0.08

View File

@ -911,7 +911,8 @@ public class StickerPickerScreen: ViewController {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: false, useOpaqueTheme: false,
hideBackground: true, hideBackground: true,
stateContext: nil stateContext: nil,
addImage: nil
) )
var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? var stickerPeekBehavior: EmojiContentPeekBehaviorImpl?
@ -1168,7 +1169,10 @@ public class StickerPickerScreen: ViewController {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: false, useOpaqueTheme: false,
hideBackground: true, hideBackground: true,
stateContext: nil stateContext: nil,
addImage: {
}
) )
if let (layout, navigationHeight) = self.currentLayout { if let (layout, navigationHeight) = self.currentLayout {

View File

@ -1595,7 +1595,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
externalExpansionView: self.view, externalExpansionView: self.view,
useOpaqueTheme: false, useOpaqueTheme: false,
hideBackground: false, hideBackground: false,
stateContext: nil stateContext: nil,
addImage: nil
) )
} }

View File

@ -688,7 +688,8 @@ final class AvatarEditorScreenComponent: Component {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: true, useOpaqueTheme: true,
hideBackground: true, hideBackground: true,
stateContext: nil stateContext: nil,
addImage: nil
) )
data.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( data.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
@ -816,7 +817,8 @@ final class AvatarEditorScreenComponent: Component {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: true, useOpaqueTheme: true,
hideBackground: true, hideBackground: true,
stateContext: nil stateContext: nil,
addImage: nil
) )
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)

View File

@ -81,6 +81,8 @@ public final class CameraButton: Component {
super.init(frame: frame) super.init(frame: frame)
self.isExclusiveTouch = true
self.addSubview(self.contentView) self.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)

View File

@ -377,6 +377,9 @@ private final class CameraScreenComponent: CombinedComponent {
if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.0 { if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.0 {
return return
} }
if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 {
return
}
self.lastFlipTimestamp = currentTimestamp self.lastFlipTimestamp = currentTimestamp
self.camera.togglePosition() self.camera.togglePosition()
@ -391,6 +394,9 @@ private final class CameraScreenComponent: CombinedComponent {
if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 { if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 {
return return
} }
if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.0 {
return
}
self.lastDualCameraTimestamp = currentTimestamp self.lastDualCameraTimestamp = currentTimestamp
let isEnabled = !self.cameraState.isDualCamEnabled let isEnabled = !self.cameraState.isDualCamEnabled
@ -531,123 +537,7 @@ private final class CameraScreenComponent: CombinedComponent {
controlsBottomInset = -48.0 controlsBottomInset = -48.0
} }
} }
let topControlInset: CGFloat = 20.0
if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "cancel",
component: AnyComponent(
Image(
image: state.image(.cancel),
size: CGSize(width: 40.0, height: 40.0)
)
)
),
action: {
guard let controller = controller() as? CameraScreen else {
return
}
controller.requestDismiss(animated: true)
}
).tagged(cancelButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + cancelButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
let flashContentComponent: AnyComponentWithIdentity<Empty>
if component.hasAppeared {
let flashIconName: String
switch state.cameraState.flashMode {
case .off:
flashIconName = "flash_off"
case .on:
flashIconName = "flash_on"
case .auto:
flashIconName = "flash_auto"
@unknown default:
flashIconName = "flash_off"
}
flashContentComponent = AnyComponentWithIdentity(
id: "animatedIcon",
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: flashIconName,
mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
range: nil,
waitForCompletion: false
),
colors: [:],
size: CGSize(width: 40.0, height: 40.0)
)
)
)
} else {
flashContentComponent = AnyComponentWithIdentity(
id: "staticIcon",
component: AnyComponent(
BundleIconComponent(
name: "Camera/FlashOffIcon",
tintColor: nil
)
)
)
}
let flashButton = flashButton.update(
component: CameraButton(
content: flashContentComponent,
action: { [weak state] in
guard let state else {
return
}
state.toggleFlashMode()
}
).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(flashButton
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
if !isTablet && Camera.isDualCamSupported {
let dualButton = dualButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "dual",
component: AnyComponent(
DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
)
),
action: { [weak state] in
guard let state else {
return
}
state.toggleDualCamera()
}
).tagged(dualButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(dualButton
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 52.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 1.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
}
}
if case .holding = state.cameraState.recording { if case .holding = state.cameraState.recording {
} else { } else {
@ -771,6 +661,122 @@ private final class CameraScreenComponent: CombinedComponent {
.position(captureControlsPosition) .position(captureControlsPosition)
) )
let topControlInset: CGFloat = 20.0
if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "cancel",
component: AnyComponent(
Image(
image: state.image(.cancel),
size: CGSize(width: 40.0, height: 40.0)
)
)
),
action: {
guard let controller = controller() as? CameraScreen else {
return
}
controller.requestDismiss(animated: true)
}
).tagged(cancelButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + cancelButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
let flashContentComponent: AnyComponentWithIdentity<Empty>
if component.hasAppeared {
let flashIconName: String
switch state.cameraState.flashMode {
case .off:
flashIconName = "flash_off"
case .on:
flashIconName = "flash_on"
case .auto:
flashIconName = "flash_auto"
@unknown default:
flashIconName = "flash_off"
}
flashContentComponent = AnyComponentWithIdentity(
id: "animatedIcon",
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: flashIconName,
mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
range: nil,
waitForCompletion: false
),
colors: [:],
size: CGSize(width: 40.0, height: 40.0)
)
)
)
} else {
flashContentComponent = AnyComponentWithIdentity(
id: "staticIcon",
component: AnyComponent(
BundleIconComponent(
name: "Camera/FlashOffIcon",
tintColor: nil
)
)
)
}
let flashButton = flashButton.update(
component: CameraButton(
content: flashContentComponent,
action: { [weak state] in
guard let state else {
return
}
state.toggleFlashMode()
}
).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(flashButton
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
if !isTablet && Camera.isDualCamSupported {
let dualButton = dualButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "dual",
component: AnyComponent(
DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
)
),
action: { [weak state] in
guard let state else {
return
}
state.toggleDualCamera()
}
).tagged(dualButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(dualButton
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 52.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 1.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
}
}
if isTablet { if isTablet {
let flipButton = flipButton.update( let flipButton = flipButton.update(
component: CameraButton( component: CameraButton(
@ -820,9 +826,9 @@ private final class CameraScreenComponent: CombinedComponent {
if isTablet { if isTablet {
timePosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 - 97.0) timePosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 - 97.0)
} else { } else {
timePosition = CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0) timePosition = CGPoint(x: availableSize.width / 2.0, y: max(environment.statusBarHeight + 5.0 + 20.0, environment.safeInsets.top + topControlInset + 20.0))
} }
if state.cameraState.recording != .none { if state.cameraState.recording != .none {
let timeBackground = timeBackground.update( let timeBackground = timeBackground.update(
component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0), component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0),
@ -1037,7 +1043,7 @@ public class CameraScreen: ViewController {
} }
} }
fileprivate final class Node: ViewControllerTracingNode { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: CameraScreen? private weak var controller: CameraScreen?
private let context: AccountContext private let context: AccountContext
private let updateState: ActionSlot<CameraState> private let updateState: ActionSlot<CameraState>
@ -1166,7 +1172,6 @@ public class CameraScreen: ViewController {
if let self { if let self {
if modeChange != .none { if modeChange != .none {
if case .dualCamera = modeChange, self.cameraPosition == .front { if case .dualCamera = modeChange, self.cameraPosition == .front {
} else { } else {
if let snapshot = self.mainPreviewView.snapshotView(afterScreenUpdates: false) { if let snapshot = self.mainPreviewView.snapshotView(afterScreenUpdates: false) {
self.mainPreviewView.addSubview(snapshot) self.mainPreviewView.addSubview(snapshot)
@ -1277,7 +1282,13 @@ public class CameraScreen: ViewController {
self.mainPreviewContainerView.addSubview(cloneView) self.mainPreviewContainerView.addSubview(cloneView)
} }
} else { } else {
if let cloneView = self.mainPreviewView.snapshotView(afterScreenUpdates: false) {
cloneView.frame = self.mainPreviewView.frame
self.additionalPreviewSnapshotView = cloneView
self.additionalPreviewContainerView.addSubview(cloneView)
}
if let cloneView = self.additionalPreviewView.snapshotView(afterScreenUpdates: false) { if let cloneView = self.additionalPreviewView.snapshotView(afterScreenUpdates: false) {
cloneView.frame = self.additionalPreviewView.frame
self.mainPreviewSnapshotView = cloneView self.mainPreviewSnapshotView = cloneView
self.mainPreviewContainerView.addSubview(cloneView) self.mainPreviewContainerView.addSubview(cloneView)
} }
@ -1297,6 +1308,7 @@ public class CameraScreen: ViewController {
self.changingPositionDisposable?.dispose() self.changingPositionDisposable?.dispose()
} }
private var pipPanGestureRecognizer: UIPanGestureRecognizer?
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
@ -1304,26 +1316,47 @@ public class CameraScreen: ViewController {
self.view.disablesInteractiveKeyboardGestureRecognizer = true self.view.disablesInteractiveKeyboardGestureRecognizer = true
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))
self.mainPreviewContainerView.addGestureRecognizer(pinchGestureRecognizer) self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
panGestureRecognizer.maximumNumberOfTouches = 1 panGestureRecognizer.maximumNumberOfTouches = 1
self.mainPreviewContainerView.addGestureRecognizer(panGestureRecognizer) self.previewContainerView.addGestureRecognizer(panGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.mainPreviewContainerView.addGestureRecognizer(tapGestureRecognizer) self.previewContainerView.addGestureRecognizer(tapGestureRecognizer)
let doubleGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:))) let doubleGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:)))
doubleGestureRecognizer.numberOfTapsRequired = 2 doubleGestureRecognizer.numberOfTapsRequired = 2
self.mainPreviewContainerView.addGestureRecognizer(doubleGestureRecognizer) self.previewContainerView.addGestureRecognizer(doubleGestureRecognizer)
let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:))) let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:)))
self.additionalPreviewContainerView.addGestureRecognizer(pipPanGestureRecognizer) pipPanGestureRecognizer.delegate = self
self.previewContainerView.addGestureRecognizer(pipPanGestureRecognizer)
self.pipPanGestureRecognizer = pipPanGestureRecognizer
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
self.camera.startCapture() self.camera.startCapture()
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
return false
}
return true
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: gestureRecognizer.view)
if gestureRecognizer === self.pipPanGestureRecognizer {
if !self.isDualCamEnabled {
return false
}
return self.additionalPreviewContainerView.frame.contains(location)
}
return self.hasAppeared
}
@objc private func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { @objc private func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
switch gestureRecognizer.state { switch gestureRecognizer.state {
case .changed: case .changed:
@ -1334,7 +1367,7 @@ public class CameraScreen: ViewController {
break break
} }
} }
private var isDismissing = false private var isDismissing = false
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let controller = self.controller else { guard let controller = self.controller else {
@ -1469,7 +1502,9 @@ public class CameraScreen: ViewController {
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height) let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
@ -1977,7 +2012,7 @@ public class CameraScreen: ViewController {
additionalPreviewView = self.additionalPreviewView additionalPreviewView = self.additionalPreviewView
} }
let mainPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - mainPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((previewSize.height - mainPreviewInnerSize.height) / 2.0)), size: mainPreviewInnerSize) let mainPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((previewFrame.width - mainPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((previewFrame.height - mainPreviewInnerSize.height) / 2.0)), size: mainPreviewInnerSize)
let additionalPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((circleSide - additionalPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((circleSide - additionalPreviewInnerSize.height) / 2.0)), size: additionalPreviewInnerSize) let additionalPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((circleSide - additionalPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((circleSide - additionalPreviewInnerSize.height) / 2.0)), size: additionalPreviewInnerSize)
if mainPreviewView.superview != self.mainPreviewContainerView { if mainPreviewView.superview != self.mainPreviewContainerView {

View File

@ -62,6 +62,8 @@ final class ModeComponent: Component {
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
self.isExclusiveTouch = true
self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
} }

View File

@ -1311,7 +1311,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: false, useOpaqueTheme: false,
hideBackground: false, hideBackground: false,
stateContext: self.stateContext?.emojiState stateContext: self.stateContext?.emojiState,
addImage: nil
) )
self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
@ -1609,7 +1610,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: false, useOpaqueTheme: false,
hideBackground: false, hideBackground: false,
stateContext: nil stateContext: nil,
addImage: nil
) )
@ -2499,7 +2501,8 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: false, useOpaqueTheme: false,
hideBackground: hideBackground, hideBackground: hideBackground,
stateContext: nil stateContext: nil,
addImage: nil
) )
let semaphore = DispatchSemaphore(value: 0) let semaphore = DispatchSemaphore(value: 0)

View File

@ -676,7 +676,8 @@ public final class EmojiStatusSelectionController: ViewController {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: true, useOpaqueTheme: true,
hideBackground: false, hideBackground: false,
stateContext: nil stateContext: nil,
addImage: nil
) )
strongSelf.refreshLayout(transition: .immediate) strongSelf.refreshLayout(transition: .immediate)

View File

@ -2322,6 +2322,7 @@ public final class EmojiPagerContentComponent: Component {
public let hideBackground: Bool public let hideBackground: Bool
public let scrollingStickersGridPromise = ValuePromise<Bool>(false) public let scrollingStickersGridPromise = ValuePromise<Bool>(false)
public let stateContext: StateContext? public let stateContext: StateContext?
public let addImage: (() -> Void)?
public init( public init(
performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void, performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void,
@ -2347,7 +2348,8 @@ public final class EmojiPagerContentComponent: Component {
externalExpansionView: UIView?, externalExpansionView: UIView?,
useOpaqueTheme: Bool, useOpaqueTheme: Bool,
hideBackground: Bool, hideBackground: Bool,
stateContext: StateContext? stateContext: StateContext?,
addImage: (() -> Void)?
) { ) {
self.performItemAction = performItemAction self.performItemAction = performItemAction
self.deleteBackwards = deleteBackwards self.deleteBackwards = deleteBackwards
@ -2373,6 +2375,7 @@ public final class EmojiPagerContentComponent: Component {
self.useOpaqueTheme = useOpaqueTheme self.useOpaqueTheme = useOpaqueTheme
self.hideBackground = hideBackground self.hideBackground = hideBackground
self.stateContext = stateContext self.stateContext = stateContext
self.addImage = addImage
} }
} }

View File

@ -409,7 +409,8 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: true, useOpaqueTheme: true,
hideBackground: false, hideBackground: false,
stateContext: nil stateContext: nil,
addImage: nil
) )
self.dataDisposable = ( self.dataDisposable = (

View File

@ -690,6 +690,18 @@ public final class EntityKeyboardComponent: Component {
} }
).minSize(CGSize(width: 38.0, height: 38.0))))) ).minSize(CGSize(width: 38.0, height: 38.0)))))
} }
if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage {
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "image", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Media Editor/AddImage",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
addImage()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
} }
if let _ = deleteBackwards { if let _ = deleteBackwards {

View File

@ -950,7 +950,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
externalExpansionView: nil, externalExpansionView: nil,
useOpaqueTheme: true, useOpaqueTheme: true,
hideBackground: false, hideBackground: false,
stateContext: nil stateContext: nil,
addImage: nil
) )
} }
} }

View File

@ -85,7 +85,21 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
public var baseSize: CGSize { public var baseSize: CGSize {
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.25) let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.25)
return CGSize(width: size, height: size)
let dimensions: CGSize
switch self.content {
case let .image(image, _):
dimensions = image.size
case let .file(file):
dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
case let .video(_, image, _):
dimensions = image?.size ?? CGSize(width: 512.0, height: 512.0)
case .dualVideoReference:
dimensions = CGSize(width: 512.0, height: 512.0)
}
let boundingSize = CGSize(width: size, height: size)
return dimensions.fitted(boundingSize)
} }
public var isAnimated: Bool { public var isAnimated: Bool {

View File

@ -269,6 +269,9 @@ private func makeEditorImageFrameComposition(context: CIContext, inputImage: CII
var baseScale: CGFloat = 1.0 var baseScale: CGFloat = 1.0
if let baseSize = entity.baseSize { if let baseSize = entity.baseSize {
baseScale = baseSize.width / image.extent.width baseScale = baseSize.width / image.extent.width
if baseSize.width != baseSize.height {
baseScale *= min(baseSize.width, baseSize.height) / max(baseSize.width, baseSize.height)
}
} }
var transform = CGAffineTransform.identity var transform = CGAffineTransform.identity

View File

@ -126,6 +126,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
switch content { switch content {
case let .file(file): case let .file(file):
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.isAnimated = true self.isAnimated = true
self.isVideoSticker = file.isVideoSticker || file.mimeType == "video/webm" self.isVideoSticker = file.isVideoSticker || file.mimeType == "video/webm"
@ -133,7 +134,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideoSticker) self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideoSticker)
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
if let source = self.source { if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fitToSize: CGSize let fitToSize: CGSize
if self.isStatic { if self.isStatic {
fitToSize = CGSize(width: 768, height: 768) fitToSize = CGSize(width: 768, height: 768)
@ -334,6 +334,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
let options = NSMutableDictionary() let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer? var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate( CVPixelBufferCreate(
kCFAllocatorDefault, kCFAllocatorDefault,

View File

@ -383,6 +383,9 @@ public final class MediaEditorValues: Codable, Equatable {
if self.videoTrimRange != nil { if self.videoTrimRange != nil {
return true return true
} }
if self.drawing != nil {
return true
}
if !self.entities.isEmpty { if !self.entities.isEmpty {
return true return true
} }

View File

@ -930,10 +930,10 @@ final class MediaEditorScreenComponent: Component {
} }
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - scrubberInset * 2.0, height: availableSize.height) containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height)
) )
let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0 + controlsBottomInset), size: scrubberSize) let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0 + controlsBottomInset), size: scrubberSize)
if let scrubberView = self.scrubber.view { if let scrubberView = self.scrubber.view {
if scrubberView.superview == nil { if scrubberView.superview == nil {
if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil {

View File

@ -928,6 +928,7 @@ public final class MessageInputPanelComponent: Component {
} }
self.currentMediaInputIsVoice = !self.currentMediaInputIsVoice self.currentMediaInputIsVoice = !self.currentMediaInputIsVoice
self.hapticFeedback.impact(.medium)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}, },
updateMediaCancelFraction: { [weak self] mediaCancelFraction in updateMediaCancelFraction: { [weak self] mediaCancelFraction in

View File

@ -139,6 +139,105 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
} }
} }
private final class StoryPinchGesture: UIPinchGestureRecognizer {
private final class Target {
var updated: (() -> Void)?
@objc func onGesture(_ gesture: UIPinchGestureRecognizer) {
self.updated?()
}
}
private let target: Target
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
var began: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
var ended: (() -> Void)?
private var initialLocation: CGPoint?
private var pinchLocation = CGPoint()
private var currentOffset = CGPoint()
private var currentNumberOfTouches = 0
init() {
self.target = Target()
super.init(target: self.target, action: #selector(self.target.onGesture(_:)))
self.target.updated = { [weak self] in
self?.gestureUpdated()
}
}
override func reset() {
super.reset()
self.currentNumberOfTouches = 0
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
//self.currentTouches.formUnion(touches)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
}
private func gestureUpdated() {
switch self.state {
case .began:
self.currentOffset = CGPoint()
let pinchLocation = self.location(in: self.view)
self.pinchLocation = pinchLocation
self.initialLocation = pinchLocation
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.currentNumberOfTouches = self.numberOfTouches
self.began?()
case .changed:
let locationSum = self.location(in: self.view)
if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 {
self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y)
}
self.currentNumberOfTouches = self.numberOfTouches
if let initialLocation = self.initialLocation {
self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y)
}
if let (scale, pinchLocation, _) = self.currentTransform {
self.currentTransform = (scale, pinchLocation, self.currentOffset)
self.updated?(scale, pinchLocation, self.currentOffset)
}
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.updated?(scale, self.pinchLocation, self.currentOffset)
case .ended, .cancelled:
self.ended?()
default:
break
}
}
}
private final class StoryContainerScreenComponent: Component { private final class StoryContainerScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -351,7 +450,28 @@ private final class StoryContainerScreenComponent: Component {
self.longPressRecognizer = longPressRecognizer self.longPressRecognizer = longPressRecognizer
self.addGestureRecognizer(longPressRecognizer) self.addGestureRecognizer(longPressRecognizer)
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:))) let pinchRecognizer = StoryPinchGesture()
pinchRecognizer.delegate = self
pinchRecognizer.updated = { [weak self] scale, pinchLocation, offset in
guard let self else {
return
}
var pinchLocation = pinchLocation
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
pinchLocation = self.convert(pinchLocation, to: itemSetComponentView)
}
}
self.itemSetPinchState = StoryItemSetContainerComponent.PinchState(scale: scale, location: pinchLocation, offset: offset)
self.state?.updated(transition: .immediate)
}
pinchRecognizer.ended = { [weak self] in
guard let self else {
return
}
self.itemSetPinchState = nil
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
self.addGestureRecognizer(pinchRecognizer) self.addGestureRecognizer(pinchRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
@ -431,6 +551,13 @@ private final class StoryContainerScreenComponent: Component {
self.volumeButtonsListenerShouldBeActiveDisposable?.dispose() self.volumeButtonsListenerShouldBeActiveDisposable?.dispose()
} }
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is StoryPinchGesture {
return !hasFirstResponder(self)
}
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return false return false
@ -581,10 +708,21 @@ private final class StoryContainerScreenComponent: Component {
@objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) { @objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state { switch recognizer.state {
case .began: case .began:
self.dismissAllTooltips()
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
if itemSetComponentView.hasActiveDeactivateableInput() {
itemSetComponentView.deactivateInput()
recognizer.isEnabled = false
recognizer.isEnabled = true
return
}
}
}
self.verticalPanState = ItemSetPanState(fraction: 0.0, didBegin: true) self.verticalPanState = ItemSetPanState(fraction: 0.0, didBegin: true)
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
self.dismissAllTooltips()
case .changed: case .changed:
let translation = recognizer.translation(in: self) let translation = recognizer.translation(in: self)
self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true) self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true)
@ -656,34 +794,17 @@ private final class StoryContainerScreenComponent: Component {
guard case .recognized = recognizer.state else { guard case .recognized = recognizer.state else {
return return
} }
let location = recognizer.location(in: recognizer.view) let location = recognizer.location(in: recognizer.view)
if let currentItemView = self.visibleItemSetViews.first?.value { if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
if location.x < currentItemView.frame.minX { if currentItemView.hasActiveDeactivateableInput() {
self.navigate(direction: .previous) currentItemView.deactivateInput()
} else if location.x > currentItemView.frame.maxX {
self.navigate(direction: .next)
}
}
}
@objc private func pinchGesture(_ recognizer: UIPinchGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
let location = recognizer.location(in: self)
let scale = max(1.0, recognizer.scale)
if let itemSetPinchState = self.itemSetPinchState {
let offset = CGPoint(x: location.x - itemSetPinchState.location.x , y: location.y - itemSetPinchState.location.y)
self.itemSetPinchState = StoryItemSetContainerComponent.PinchState(scale: scale, location: itemSetPinchState.location, offset: offset)
} else { } else {
self.itemSetPinchState = StoryItemSetContainerComponent.PinchState(scale: scale, location: location, offset: .zero) if location.x < currentItemView.frame.minX {
self.navigate(direction: .previous)
} else if location.x > currentItemView.frame.maxX {
self.navigate(direction: .next)
}
} }
self.state?.updated(transition: .immediate)
case .cancelled, .ended:
self.itemSetPinchState = nil
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
default:
break
} }
} }
@ -996,6 +1117,7 @@ private final class StoryContainerScreenComponent: Component {
} }
} }
var presentationContextInsets = UIEdgeInsets()
if !currentSlices.isEmpty, let focusedIndex { if !currentSlices.isEmpty, let focusedIndex {
for i in max(0, focusedIndex - 1) ... min(focusedIndex + 1, currentSlices.count - 1) { for i in max(0, focusedIndex - 1) ... min(focusedIndex + 1, currentSlices.count - 1) {
var isItemVisible = false var isItemVisible = false
@ -1053,6 +1175,10 @@ private final class StoryContainerScreenComponent: Component {
itemSetContainerInsets.top = 0.0 itemSetContainerInsets.top = 0.0
itemSetContainerInsets.bottom = floorToScreenPixels((availableSize.height - itemSetContainerSize.height) / 2.0) itemSetContainerInsets.bottom = floorToScreenPixels((availableSize.height - itemSetContainerSize.height) / 2.0)
itemSetContainerSafeInsets.bottom = 0.0 itemSetContainerSafeInsets.bottom = 0.0
presentationContextInsets.left = floorToScreenPixels((availableSize.width - itemSetContainerSize.width) / 2.0)
presentationContextInsets.right = presentationContextInsets.left
presentationContextInsets.bottom = itemSetContainerInsets.bottom
} }
let _ = itemSetView.view.update( let _ = itemSetView.view.update(
@ -1327,8 +1453,8 @@ private final class StoryContainerScreenComponent: Component {
size: availableSize, size: availableSize,
metrics: environment.metrics, metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics, deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: contentDerivedBottomInset, right: 0.0), intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: contentDerivedBottomInset + presentationContextInsets.bottom, right: 0.0),
safeInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(top: 0.0, left: presentationContextInsets.left, bottom: 0.0, right: presentationContextInsets.right),
additionalInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(),
statusBarHeight: nil, statusBarHeight: nil,
inputHeight: nil, inputHeight: nil,

View File

@ -663,25 +663,30 @@ public final class StoryItemSetContainerComponent: Component {
return false return false
} }
private func deactivateInput() { func hasActiveDeactivateableInput() -> Bool {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
if view.canDeactivateInput() && (hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media) {
return true
}
}
return false
}
func deactivateInput() {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View, view.canDeactivateInput() {
self.sendMessageContext.currentInputMode = .text
if hasFirstResponder(self) {
view.deactivateInput()
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
}
}
} }
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout { if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
if hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media { if hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View { self.deactivateInput()
if view.canDeactivateInput() {
self.sendMessageContext.currentInputMode = .text
if hasFirstResponder(self) {
view.deactivateInput()
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
}
} else {
view.animateError()
}
}
} else if self.displayViewList { } else if self.displayViewList {
let point = recognizer.location(in: self) let point = recognizer.location(in: self)
@ -2273,7 +2278,7 @@ public final class StoryItemSetContainerComponent: Component {
let tooltipScreen = TooltipScreen( let tooltipScreen = TooltipScreen(
account: component.context.account, account: component.context.account,
sharedContext: component.context.sharedContext, sharedContext: component.context.sharedContext,
text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: nil).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: true) return .dismiss(consume: true)
} }
) )
@ -3174,9 +3179,6 @@ public final class StoryItemSetContainerComponent: Component {
} }
let subject: Signal<MediaEditorScreen.Subject?, NoError> let subject: Signal<MediaEditorScreen.Subject?, NoError>
// if let source {
// subject = .single(.draft(source, Int64(id)))
// } else {
var duration: Double? var duration: Double?
let media = item.media._asMedia() let media = item.media._asMedia()

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "photo_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,154 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.835083 3.834961 cm
1.000000 1.000000 1.000000 scn
5.465000 16.330017 m
5.436178 16.330017 l
5.436174 16.330017 l
4.620535 16.330023 3.967872 16.330027 3.440454 16.286936 c
2.899074 16.242702 2.431364 16.149773 2.001125 15.930555 c
1.311511 15.579180 0.750837 15.018506 0.399461 14.328892 c
0.180244 13.898653 0.087314 13.430943 0.043081 12.889563 c
-0.000010 12.362144 -0.000006 11.709482 0.000000 10.893843 c
0.000000 10.893839 l
0.000000 10.865017 l
0.000000 5.465017 l
0.000000 5.436195 l
-0.000006 4.620555 -0.000010 3.967890 0.043081 3.440471 c
0.087314 2.899090 0.180244 2.431380 0.399461 2.001142 c
0.750837 1.311528 1.311511 0.750854 2.001125 0.399478 c
2.431364 0.180260 2.899074 0.087330 3.440454 0.043098 c
3.967863 0.000008 4.620512 0.000011 5.436130 0.000017 c
5.436194 0.000017 l
5.465001 0.000017 l
10.865000 0.000017 l
10.893806 0.000017 l
10.893871 0.000017 l
11.709489 0.000011 12.362138 0.000008 12.889546 0.043098 c
13.430927 0.087330 13.898637 0.180260 14.328876 0.399478 c
15.018489 0.750854 15.579163 1.311528 15.930539 2.001142 c
16.149757 2.431380 16.242687 2.899091 16.286919 3.440471 c
16.330009 3.967880 16.330006 4.620529 16.330000 5.436146 c
16.330000 5.436212 l
16.330000 5.465017 l
16.330000 10.865017 l
16.330000 10.893824 l
16.330000 10.893888 l
16.330006 11.709505 16.330009 12.362154 16.286919 12.889563 c
16.242687 13.430943 16.149757 13.898653 15.930539 14.328892 c
15.579163 15.018506 15.018489 15.579180 14.328876 15.930555 c
13.898637 16.149773 13.430926 16.242702 12.889546 16.286936 c
12.362126 16.330029 11.709461 16.330023 10.893822 16.330017 c
10.865000 16.330017 l
5.465000 16.330017 l
h
2.604932 14.745517 m
2.816429 14.853280 3.089626 14.923841 3.548759 14.961352 c
4.015654 14.999499 4.613948 15.000017 5.465000 15.000017 c
10.865000 15.000017 l
11.716052 15.000017 12.314346 14.999499 12.781241 14.961352 c
13.240374 14.923841 13.513572 14.853280 13.725068 14.745517 c
14.164426 14.521652 14.521636 14.164443 14.745501 13.725084 c
14.853263 13.513588 14.923823 13.240391 14.961336 12.781259 c
14.999483 12.314363 15.000000 11.716068 15.000000 10.865017 c
15.000000 5.465017 l
15.000000 5.137442 14.999924 4.847313 14.997654 4.587710 c
12.903418 6.817746 l
12.230759 7.534022 11.087341 7.515037 10.438833 6.776826 c
9.645941 5.874260 l
5.897148 9.831320 l
5.226022 10.539732 4.092310 10.521740 3.444000 9.792391 c
1.330000 7.414142 l
1.330000 10.865017 l
1.330000 11.716068 1.330518 12.314363 1.368664 12.781259 c
1.406177 13.240391 1.476737 13.513588 1.584500 13.725084 c
1.808364 14.164443 2.165574 14.521652 2.604932 14.745517 c
h
11.933909 5.907277 m
14.833861 2.819281 l
14.807792 2.739742 14.778378 2.669474 14.745501 2.604949 c
14.521636 2.165591 14.164426 1.808381 13.725068 1.584517 c
13.714809 1.579343 l
10.564494 4.904676 l
11.438032 5.899043 l
11.568513 6.047573 11.798571 6.051393 11.933909 5.907277 c
h
1.330001 5.412228 m
1.330038 4.589128 1.331311 4.005958 1.368664 3.548775 c
1.406177 3.089643 1.476737 2.816445 1.584500 2.604949 c
1.808364 2.165591 2.165574 1.808381 2.604932 1.584517 c
2.816429 1.476754 3.089626 1.406194 3.548759 1.368681 c
4.015654 1.330534 4.613949 1.330017 5.465001 1.330017 c
10.865000 1.330017 l
11.357291 1.330017 11.765009 1.330190 12.111642 1.337719 c
4.931631 8.916620 l
4.796600 9.059153 4.568496 9.055533 4.438055 8.908787 c
1.330001 5.412228 l
h
11.664969 10.165024 m
12.493397 10.165024 13.164969 10.836597 13.164969 11.665024 c
13.164969 12.493451 12.493397 13.165024 11.664969 13.165024 c
10.836542 13.165024 10.164969 12.493451 10.164969 11.665024 c
10.164969 10.836597 10.836542 10.165024 11.664969 10.165024 c
h
f*
n
Q
endstream
endobj
3 0 obj
3694
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003784 00000 n
0000003807 00000 n
0000003980 00000 n
0000004054 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4113
%%EOF

View File

@ -8067,7 +8067,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .proxy: case .proxy:
self.controller?.push(proxySettingsController(context: self.context)) self.controller?.push(proxySettingsController(context: self.context))
case .stories: case .stories:
self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved)) push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved))
case .savedMessages: case .savedMessages:
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
@ -8247,7 +8247,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .emojiStatus: case .emojiStatus:
self.headerNode.invokeDisplayPremiumIntro() self.headerNode.invokeDisplayPremiumIntro()
case .powerSaving: case .powerSaving:
self.controller?.push(energySavingSettingsScreen(context: self.context)) push(energySavingSettingsScreen(context: self.context))
} }
} }