Merge commit '74de28b1547b9bf729163034bcdc918819a86d85'

# Conflicts:
#	submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift
This commit is contained in:
Ali 2023-07-09 02:09:22 +04:00
commit 66a8c7e4a7
23 changed files with 577 additions and 343 deletions

View File

@ -10,7 +10,7 @@ final class CameraSession {
private let multiSession: Any? private let multiSession: Any?
let hasMultiCam: Bool 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()
@ -21,6 +21,7 @@ final class CameraSession {
self.multiSession = nil self.multiSession = nil
self.hasMultiCam = false self.hasMultiCam = false
} }
self.session.sessionPreset = .inputPriority
} }
var session: AVCaptureSession { var session: AVCaptureSession {
@ -64,10 +65,10 @@ final class CameraDeviceContext {
self.previewView = previewView self.previewView = previewView
self.device.configure(for: session, position: position, dual: !exclusive || additional) self.device.configure(for: session, position: position, dual: !exclusive || additional)
self.device.configureDeviceFormat(maxDimensions: self.preferredMaxDimensions, maxFramerate: self.preferredMaxFrameRate)
self.input.configure(for: session, device: self.device, audio: audio) self.input.configure(for: session, device: self.device, audio: audio)
self.output.configure(for: session, device: self.device, input: self.input, previewView: previewView, audio: audio, photo: photo, metadata: metadata) self.output.configure(for: session, device: self.device, input: self.input, previewView: previewView, audio: audio, photo: photo, metadata: metadata)
self.device.configureDeviceFormat(maxDimensions: self.preferredMaxDimensions, maxFramerate: self.preferredMaxFrameRate)
self.output.configureVideoStabilization() self.output.configureVideoStabilization()
self.device.resetZoom(neutral: self.exclusive || !self.additional) self.device.resetZoom(neutral: self.exclusive || !self.additional)
@ -83,8 +84,6 @@ final class CameraDeviceContext {
private var preferredMaxDimensions: CMVideoDimensions { private var preferredMaxDimensions: CMVideoDimensions {
if self.additional { if self.additional {
//if case .iPhoneXS = DeviceModel.current {
// return CMVideoDimensions(width: 1440, height: 1080)
return CMVideoDimensions(width: 1920, height: 1440) return CMVideoDimensions(width: 1920, height: 1440)
} else { } else {
return CMVideoDimensions(width: 1920, height: 1080) return CMVideoDimensions(width: 1920, height: 1080)

View File

@ -44,11 +44,8 @@ final class CameraDevice {
selectedDevice = device selectedDevice = device
} }
} else { } else {
if #available(iOS 11.1, *), dual, case .front = position, let trueDepthDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInTrueDepthCamera], mediaType: .video, position: position).devices.first {
selectedDevice = trueDepthDevice
}
if selectedDevice == nil { if selectedDevice == nil {
selectedDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first selectedDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first
} }
} }

View File

@ -169,7 +169,7 @@ final class CameraOutput: NSObject {
} else { } else {
Logger.shared.log("Camera", "Can't add video connection") Logger.shared.log("Camera", "Can't add video connection")
} }
if photo { if photo {
let photoConnection = AVCaptureConnection(inputPorts: ports, output: self.photoOutput) let photoConnection = AVCaptureConnection(inputPorts: ports, output: self.photoOutput)
if session.session.canAddConnection(photoConnection) { if session.session.canAddConnection(photoConnection) {

View File

@ -689,48 +689,6 @@ public class ContactsController: ViewController {
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ContactsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
} }
private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator?
var hasStoryCameraTransition: Bool {
return self.storyCameraTransitionInCoordinator != nil
}
func storyCameraPanGestureChanged(transitionFraction: CGFloat) {
guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else {
return
}
let coordinator: StoryCameraTransitionInCoordinator?
if let current = self.storyCameraTransitionInCoordinator {
coordinator = current
} else {
coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in
guard let self else {
return nil
}
let _ = self
// if finished, let componentView = self.chatListHeaderView() {
// if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
// return StoryCameraTransitionOut(
// destinationView: transitionView,
// destinationRect: transitionView.bounds,
// destinationCornerRadius: transitionView.bounds.height * 0.5
// )
// }
// }
return nil
})
self.storyCameraTransitionInCoordinator = coordinator
}
coordinator?.updateTransitionProgress(transitionFraction)
}
func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) {
if let coordinator = self.storyCameraTransitionInCoordinator {
coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity)
self.storyCameraTransitionInCoordinator = nil
}
}
} }
private final class ContactsTabBarContextExtractedContentSource: ContextExtractedContentSource { private final class ContactsTabBarContextExtractedContentSource: ContextExtractedContentSource {

View File

@ -68,10 +68,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
private let stringsPromise = Promise<PresentationStrings>() private let stringsPromise = Promise<PresentationStrings>()
private var isStoryPostingAvailable = false
private var storiesPostingAvailabilityDisposable: Disposable?
weak var controller: ContactsController? weak var controller: ContactsController?
private var initialScrollingOffset: CGFloat? private var initialScrollingOffset: CGFloat?
@ -254,80 +251,11 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
} }
self.openStories?(peer, sourceNode) self.openStories?(peer, sourceNode)
} }
let storiesPostingAvailability = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> AppConfiguration in
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
return appConfiguration
}
|> distinctUntilChanged
|> map { appConfiguration -> StoriesConfiguration.PostingAvailability in
let storiesConfiguration = StoriesConfiguration.with(appConfiguration: appConfiguration)
return storiesConfiguration.posting
}
self.storiesPostingAvailabilityDisposable = combineLatest(queue: Queue.mainQueue(),
storiesPostingAvailability,
self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> map { peer -> Bool in
if case let .user(user) = peer, user.isPremium {
return true
} else {
return false
}
}
|> distinctUntilChanged
).start(next: { [weak self] postingAvailability, isPremium in
if let self {
let isStoryPostingAvailable: Bool
switch postingAvailability {
case .enabled:
isStoryPostingAvailable = true
case .premium:
isStoryPostingAvailable = isPremium
case .disabled:
isStoryPostingAvailable = false
}
self.isStoryPostingAvailable = isStoryPostingAvailable
}
})
} }
deinit { deinit {
self.presentationDataDisposable?.dispose() self.presentationDataDisposable?.dispose()
self.storySubscriptionsDisposable?.dispose() self.storySubscriptionsDisposable?.dispose()
self.storiesPostingAvailabilityDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { _ in
return [.rightCenter, .rightEdge]
}, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer)
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return self.isStoryPostingAvailable
} }
private func updateThemeAndStrings() { private func updateThemeAndStrings() {
@ -594,38 +522,6 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
placeholderNode.frame = previousFrame placeholderNode.frame = previousFrame
} }
} }
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard let (layout, _) = self.containerLayout else {
return
}
switch recognizer.state {
case .began:
break
case .changed:
let translation = recognizer.translation(in: self.view)
if case .compact = layout.metrics.widthClass {
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
if translation.x > 0.0 {
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
}
if cameraIsAlreadyOpened {
return
}
}
case .cancelled, .ended:
let translation = recognizer.translation(in: self.view)
let velocity = recognizer.velocity(in: self.view)
let hasStoryCameraTransition = self.controller?.hasStoryCameraTransition ?? false
if hasStoryCameraTransition {
self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
}
default:
break
}
}
} }
private final class ContactContextExtractedContentSource: ContextExtractedContentSource { private final class ContactContextExtractedContentSource: ContextExtractedContentSource {

View File

@ -96,6 +96,7 @@ swift_library(
"//submodules/TelegramUI/Components/MediaEditor", "//submodules/TelegramUI/Components/MediaEditor",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI", "//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
"//submodules/TelegramUI/Components/LottieComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -215,6 +215,10 @@ final class DrawingBubbleEntitySelectionView: DrawingEntitySelectionView {
self.currentHandle = self.layer self.currentHandle = self.layer
entityView.onInteractionUpdated(true) entityView.onInteractionUpdated(true)
case .changed: case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview) let delta = gestureRecognizer.translation(in: entityView.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview)

View File

@ -2,8 +2,11 @@ import Foundation
import UIKit import UIKit
import Display import Display
import LegacyComponents import LegacyComponents
import SwiftSignalKit
import AccountContext import AccountContext
import MediaEditor import MediaEditor
import ComponentFlow
import LottieAnimationComponent
public func decodeDrawingEntities(data: Data) -> [DrawingEntity] { public func decodeDrawingEntities(data: Data) -> [DrawingEntity] {
if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) { if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) {
@ -75,6 +78,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let xAxisView = UIView() private let xAxisView = UIView()
private let yAxisView = UIView() private let yAxisView = UIView()
private let angleLayer = SimpleShapeLayer() private let angleLayer = SimpleShapeLayer()
private let bin = ComponentView<Empty>()
public var onInteractionUpdated: (Bool) -> Void = { _ in } public var onInteractionUpdated: (Bool) -> Void = { _ in }
public var edgePreviewUpdated: (Bool) -> Void = { _ in } public var edgePreviewUpdated: (Bool) -> Void = { _ in }
@ -582,17 +586,19 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
@objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) { @objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) {
let location = gestureRecognzier.location(in: self) let location = gestureRecognzier.location(in: self)
if let entityView = self.entity(at: location) {
self.selectEntity(entityView.entity)
}
}
private func entity(at location: CGPoint) -> DrawingEntityView? {
var intersectedViews: [DrawingEntityView] = [] var intersectedViews: [DrawingEntityView] = []
for case let view as DrawingEntityView in self.subviews { for case let view as DrawingEntityView in self.subviews {
if view.precisePoint(inside: self.convert(location, to: view)) { if view.precisePoint(inside: self.convert(location, to: view)) {
intersectedViews.append(view) intersectedViews.append(view)
} }
} }
return intersectedViews.last
if let entityView = intersectedViews.last {
self.selectEntity(entityView.entity)
}
} }
public func selectEntity(_ entity: DrawingEntity?) { public func selectEntity(_ entity: DrawingEntity?) {
@ -622,8 +628,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
if let selectionView = entityView.makeSelectionView() { if let selectionView = entityView.makeSelectionView() {
selectionView.tapped = { [weak self, weak entityView] in selectionView.tapped = { [weak self, weak entityView] in
if let strongSelf = self, let entityView = entityView { if let self, let entityView = entityView {
strongSelf.requestedMenuForEntityView(entityView, strongSelf.subviews.last === entityView) let entityViews = self.subviews.filter { $0 is DrawingEntityView }
self.requestedMenuForEntityView(entityView, entityViews.last === entityView)
} }
} }
entityView.selectionView = selectionView entityView.selectionView = selectionView
@ -679,10 +686,74 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
return false return false
} }
} }
public func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { public func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
let location = gestureRecognizer.location(in: self)
if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
selectionView.handlePan(gestureRecognizer) var isTrappedInBin = false
let scale = 100.0 / selectedEntityView.bounds.size.width
switch gestureRecognizer.state {
case .changed:
if self.updateBin(location: location) {
isTrappedInBin = true
}
case .ended, .cancelled:
let _ = self.updateBin(location: nil)
if selectedEntityView.isTrappedInBin {
selectedEntityView.layer.animateScale(from: scale, to: 0.01, duration: 0.2, removeOnCompletion: false)
selectedEntityView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
self.remove(uuid: selectedEntityView.entity.uuid)
})
self.selectEntity(nil)
Queue.mainQueue().after(0.3, {
self.onInteractionUpdated(false)
})
return
}
default:
break
}
let transition = Transition.easeInOut(duration: 0.2)
if isTrappedInBin, let binView = self.bin.view {
if !selectedEntityView.isTrappedInBin {
let refs = [
self.xAxisView,
self.yAxisView,
self.topEdgeView,
self.leftEdgeView,
self.rightEdgeView,
self.bottomEdgeView
]
for ref in refs {
transition.setAlpha(view: ref, alpha: 0.0)
}
self.edgePreviewUpdated(false)
selectedEntityView.isTrappedInBin = true
transition.setAlpha(view: selectionView, alpha: 0.0)
transition.animatePosition(view: selectionView, from: selectionView.center, to: self.convert(binView.center, to: selectionView.superview))
transition.animateScale(view: selectionView, from: 0.0, to: -0.5, additive: true)
transition.setPosition(view: selectedEntityView, position: binView.center)
let rotation = selectedEntityView.layer.transform.decompose().rotation
var transform = CATransform3DMakeScale(scale, scale, 1.0)
transform = CATransform3DRotate(transform, CGFloat(rotation.z), 0.0, 0.0, 1.0)
transition.setTransform(view: selectedEntityView, transform: transform)
}
} else {
if selectedEntityView.isTrappedInBin {
selectedEntityView.isTrappedInBin = false
transition.setAlpha(view: selectionView, alpha: 1.0)
selectedEntityView.layer.animateScale(from: scale, to: selectedEntityView.entity.scale, duration: 0.13)
}
selectionView.handlePan(gestureRecognizer)
}
} else if gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
self.selectEntity(viewToSelect.entity)
} else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { } else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handlePan(gestureRecognizer) mediaEntityView.handlePan(gestureRecognizer)
} }
@ -703,6 +774,40 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
selectionView.handleRotate(gestureRecognizer) selectionView.handleRotate(gestureRecognizer)
} }
} }
private var binWasOpened = false
private func updateBin(location: CGPoint?) -> Bool {
let binSize = CGSize(width: 180.0, height: 180.0)
let binFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - binSize.width) / 2.0), y: self.bounds.height - binSize.height - 20.0), size: binSize)
let wasOpened = self.binWasOpened
var isOpened = false
if let location {
isOpened = binFrame.insetBy(dx: 20.0, dy: 20.0).contains(location)
}
self.binWasOpened = isOpened
if wasOpened != isOpened {
self.hapticFeedback.impact(.medium)
}
let _ = self.bin.update(
transition: .immediate,
component: AnyComponent(EntityBinComponent(isOpened: isOpened)),
environment: {},
containerSize: binSize
)
if let binView = self.bin.view {
if binView.superview == nil {
self.addSubview(binView)
} else if self.subviews.last !== binView {
self.bringSubviewToFront(binView)
}
binView.frame = binFrame
Transition.easeInOut(duration: 0.2).setAlpha(view: binView, alpha: location != nil ? 1.0 : 0.0, delay: location == nil && wasOpened ? 0.4 : 0.0)
}
return isOpened
}
} }
protocol DrawingEntityMediaView: DrawingEntityView { protocol DrawingEntityMediaView: DrawingEntityView {
@ -716,6 +821,8 @@ public class DrawingEntityView: UIView {
public let entity: DrawingEntity public let entity: DrawingEntity
var isTracking = false var isTracking = false
var isTrappedInBin = false
public weak var selectionView: DrawingEntitySelectionView? public weak var selectionView: DrawingEntitySelectionView?
weak var containerView: DrawingEntitiesView? weak var containerView: DrawingEntitiesView?
@ -878,3 +985,97 @@ public class DrawingSelectionContainerView: UIView {
return result return result
} }
} }
private final class EntityBinComponent: Component {
typealias EnvironmentType = Empty
let isOpened: Bool
init(
isOpened: Bool
) {
self.isOpened = isOpened
}
static func ==(lhs: EntityBinComponent, rhs: EntityBinComponent) -> Bool {
if lhs.isOpened != rhs.isOpened {
return false
}
return true
}
public final class View: UIView {
private let circle = SimpleShapeLayer()
private let animation = ComponentView<Empty>()
private var component: EntityBinComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.circle.strokeColor = UIColor.white.cgColor
self.circle.fillColor = UIColor.clear.cgColor
self.circle.lineWidth = 5.0
self.layer.addSublayer(self.circle)
self.circle.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)).insetBy(dx: 3.0, dy: 3.0), transform: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var wasOpened = false
func update(component: EntityBinComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
if !self.wasOpened {
self.wasOpened = component.isOpened
}
let animationSize = self.animation.update(
transition: transition,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_entitybin",
mode: component.isOpened ? .animating(loop: false) : (self.wasOpened ? .animating(loop: false) : .still(position: .end)),
range: component.isOpened ? (0.0, 0.5) : (0.5, 1.0)
),
colors: [:],
size: CGSize(width: 140.0, height: 140.0)
)),
environment: {},
containerSize: CGSize(width: 140.0, height: 140.0)
)
let animationFrame = CGRect(
origin: CGPoint(x: 20.0, y: 20.0),
size: animationSize
)
if let animationView = self.animation.view {
if animationView.superview == nil {
self.addSubview(animationView)
}
transition.setPosition(view: animationView, position: animationFrame.center)
transition.setBounds(view: animationView, bounds: CGRect(origin: .zero, size: animationFrame.size))
}
let circleSize = CGSize(width: 160.0, height: 160.0)
self.circle.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - circleSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - circleSize.height) / 2.0)), size: CGSize(width: 100.0, height: 100.0))
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -222,6 +222,10 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView
self.currentHandle = self.layer self.currentHandle = self.layer
entityView.onInteractionUpdated(true) entityView.onInteractionUpdated(true)
case .changed: case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview) let delta = gestureRecognizer.translation(in: entityView.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview)

View File

@ -521,6 +521,10 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView {
self.currentHandle = self.layer self.currentHandle = self.layer
entityView.onInteractionUpdated(true) entityView.onInteractionUpdated(true)
case .changed: case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview) let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview) let parentLocation = gestureRecognizer.location(in: self.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview)

View File

@ -785,6 +785,10 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView {
self.currentHandle = self.layer self.currentHandle = self.layer
entityView.onInteractionUpdated(true) entityView.onInteractionUpdated(true)
case .changed: case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview) let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview) let parentLocation = gestureRecognizer.location(in: self.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview)

View File

@ -517,3 +517,30 @@ func normalizeDrawingRect(_ rect: CGRect, drawingSize: CGSize) -> CGRect {
} }
return rect return rect
} }
extension CATransform3D {
func decompose() -> (translation: SIMD3<Float>, rotation: SIMD3<Float>, scale: SIMD3<Float>) {
let m0 = SIMD3<Float>(Float(self.m11), Float(self.m12), Float(self.m13))
let m1 = SIMD3<Float>(Float(self.m21), Float(self.m22), Float(self.m23))
let m2 = SIMD3<Float>(Float(self.m31), Float(self.m32), Float(self.m33))
let m3 = SIMD3<Float>(Float(self.m41), Float(self.m42), Float(self.m43))
let t = m3
let sx = length(m0)
let sy = length(m1)
let sz = length(m2)
let s = SIMD3<Float>(sx, sy, sz)
let rx = m0 / sx
let ry = m1 / sy
let rz = m2 / sz
let pitch = atan2(ry.z, rz.z)
let yaw = atan2(-rx.z, hypot(ry.z, rz.z))
let roll = atan2(rx.y, rx.x)
let r = SIMD3<Float>(pitch, yaw, roll)
return (t, r, s)
}
}

View File

@ -188,6 +188,10 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView {
} }
self.currentHandle = self.layer self.currentHandle = self.layer
case .changed: case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
if gestureRecognizer.numberOfTouches > 1 { if gestureRecognizer.numberOfTouches > 1 {
return return
} }

View File

@ -1242,7 +1242,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case other case other
} }
var disposed: () -> Void = {} public var disposed: () -> Void = {}
private var didSetReady = false private var didSetReady = false
private let _ready = Promise<Bool>() private let _ready = Promise<Bool>()

View File

@ -1080,7 +1080,17 @@ public class CameraScreen: ViewController {
fileprivate var previewBlurPromise = ValuePromise<Bool>(false) fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
private let animateFlipAction = ActionSlot<Void>() private let animateFlipAction = ActionSlot<Void>()
fileprivate var cameraIsActive = true private let idleTimerExtensionDisposable = MetaDisposable()
fileprivate var cameraIsActive = true {
didSet {
if self.cameraIsActive {
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
} else {
self.idleTimerExtensionDisposable.set(nil)
}
}
}
fileprivate var hasGallery = false fileprivate var hasGallery = false
private var presentationData: PresentationData private var presentationData: PresentationData
@ -1302,10 +1312,13 @@ public class CameraScreen: ViewController {
} }
} }
} }
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
} }
deinit { deinit {
self.changingPositionDisposable?.dispose() self.changingPositionDisposable?.dispose()
self.idleTimerExtensionDisposable.dispose()
} }
private var pipPanGestureRecognizer: UIPanGestureRecognizer? private var pipPanGestureRecognizer: UIPanGestureRecognizer?
@ -1731,7 +1744,7 @@ public class CameraScreen: ViewController {
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 3.0), size: CGSize()) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 3.0), size: CGSize())
let accountManager = self.context.sharedContext.accountManager let accountManager = self.context.sharedContext.accountManager
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Take photos or videos to share with all\nyour contacts or close friends at once."), textAlignment: .center, location: .point(location, .bottom), displayDuration: .custom(3.0), inset: 16.0, shouldDismissOnTouch: { point, containerFrame in let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Take photos or videos to share with all\nyour contacts or close friends at once."), textAlignment: .center, location: .point(location, .bottom), displayDuration: .custom(4.5), inset: 16.0, shouldDismissOnTouch: { point, containerFrame in
if containerFrame.contains(point) { if containerFrame.contains(point) {
let _ = ApplicationSpecificNotice.incrementStoriesCameraTip(accountManager: accountManager).start() let _ = ApplicationSpecificNotice.incrementStoriesCameraTip(accountManager: accountManager).start()
return .dismiss(consume: true) return .dismiss(consume: true)
@ -2224,6 +2237,8 @@ public class CameraScreen: ViewController {
} }
} }
self.push(controller) self.push(controller)
self.requestLayout(transition: .immediate)
} }
public func presentDraftTooltip() { public func presentDraftTooltip() {

View File

@ -1102,12 +1102,23 @@ final class CaptureControlsComponent: Component {
lockMaskFrame = lockMaskFrame.offsetBy(dx: 8.0, dy: 0.0) lockMaskFrame = lockMaskFrame.offsetBy(dx: 8.0, dy: 0.0)
} }
} }
let _ = self.lockView.update( let _ = self.lockView.update(
transition: transition, transition: .immediate,
component: AnyComponent( component: AnyComponent(
LockContentComponent( CameraButton(
maskFrame: lockMaskFrame content: AnyComponentWithIdentity(
id: "lock",
component: AnyComponent(
LockContentComponent(
maskFrame: lockMaskFrame
)
)
),
minSize: hintIconSize,
action: {
component.flipTapped()
}
) )
), ),
environment: {}, environment: {},
@ -1123,7 +1134,7 @@ final class CaptureControlsComponent: Component {
transition.setScale(view: lockView, scale: isHolding ? 1.0 : 0.1) transition.setScale(view: lockView, scale: isHolding ? 1.0 : 0.1)
transition.setAlpha(view: lockView, alpha: isHolding ? 1.0 : 0.0) transition.setAlpha(view: lockView, alpha: isHolding ? 1.0 : 0.0)
if let lockMaskView = lockView as? LockContentComponent.View { if let buttonView = lockView as? CameraButton.View, let lockMaskView = buttonView.contentView.componentView as? LockContentComponent.View {
transition.setAlpha(view: lockMaskView.maskContainerView, alpha: isHolding ? 1.0 : 0.0) transition.setAlpha(view: lockMaskView.maskContainerView, alpha: isHolding ? 1.0 : 0.0)
transition.setSublayerTransform(layer: lockMaskView.maskContainerView.layer, transform: isHolding ? CATransform3DIdentity : CATransform3DMakeScale(0.1, 0.1, 1.0)) transition.setSublayerTransform(layer: lockMaskView.maskContainerView.layer, transform: isHolding ? CATransform3DIdentity : CATransform3DMakeScale(0.1, 0.1, 1.0))
} }
@ -1155,11 +1166,11 @@ final class CaptureControlsComponent: Component {
contentView.maskContainerView.frame = contentView.convert(contentView.bounds, to: self) contentView.maskContainerView.frame = contentView.convert(contentView.bounds, to: self)
} }
if let lockView = self.lockView.view as? LockContentComponent.View { if let buttonView = self.lockView.view as? CameraButton.View, let contentView = buttonView.contentView.componentView as? LockContentComponent.View {
if lockView.maskContainerView.superview == nil { if contentView.maskContainerView.superview == nil {
self.addSubview(lockView.maskContainerView) self.addSubview(contentView.maskContainerView)
} }
lockView.maskContainerView.center = lockView.center contentView.maskContainerView.center = buttonView.center
} }
return size return size

View File

@ -2093,6 +2093,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
} }
self.isInteractingWithEntities = isInteracting self.isInteractingWithEntities = isInteracting
if !isInteracting {
self.controller?.isSavingAvailable = true
}
self.requestUpdate(transition: .easeInOut(duration: 0.2)) self.requestUpdate(transition: .easeInOut(duration: 0.2))
} }
}, },
@ -3853,11 +3856,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
if let previousSavedValues = self.previousSavedValues, mediaEditor.values == previousSavedValues {
return
}
self.hapticFeedback.impact(.light) self.hapticFeedback.impact(.light)
self.previousSavedValues = mediaEditor.values self.previousSavedValues = mediaEditor.values

View File

@ -267,9 +267,13 @@ private final class MediaToolsScreenComponent: Component {
var delay: Double = 0.0 var delay: Double = 0.0
for button in buttons { for button in buttons {
if let view = button.view { if let view = button.view {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.alpha = 0.0
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) Queue.mainQueue().after(delay, {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: delay) view.alpha = 1.0
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0)
})
delay += 0.03 delay += 0.03
} }
} }

View File

@ -74,6 +74,8 @@ swift_library(
"//submodules/StickerPackPreviewUI", "//submodules/StickerPackPreviewUI",
"//submodules/Components/AnimatedStickerComponent", "//submodules/Components/AnimatedStickerComponent",
"//submodules/OpenInExternalAppUI", "//submodules/OpenInExternalAppUI",
"//submodules/MediaPasteboardUI",
"//submodules/WebPBinding",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -173,7 +173,7 @@ public final class StoryContentContextImpl: StoryContentContext {
isExpired: false, isExpired: false,
isPublic: false, isPublic: false,
isPending: true, isPending: true,
isCloseFriends: false, isCloseFriends: item.privacy.base == .closeFriends,
isForwardingDisabled: false, isForwardingDisabled: false,
isEdited: false isEdited: false
)) ))

View File

@ -583,6 +583,12 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
if let inputMediaView = self.sendMessageContext.inputMediaNode {
if inputMediaView.frame.contains(point) {
return false
}
}
if let centerInfoItemView = self.centerInfoItem?.view.view { if let centerInfoItemView = self.centerInfoItem?.view.view {
if centerInfoItemView.convert(centerInfoItemView.bounds, to: self).contains(point) { if centerInfoItemView.convert(centerInfoItemView.bounds, to: self).contains(point) {
return false return false
@ -1579,11 +1585,7 @@ public final class StoryItemSetContainerComponent: Component {
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil let isFirstTime = self.component == nil
// if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) {
// self.sendMessageContext.currentInputMode = .text
// }
if self.component == nil { if self.component == nil {
self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData) self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData)
} }
@ -1759,7 +1761,11 @@ public final class StoryItemSetContainerComponent: Component {
return return
} }
self.sendMessageContext.toggleInputMode() self.sendMessageContext.toggleInputMode()
self.state?.updated(transition: .immediate) if !hasFirstResponder(self) {
self.state?.updated(transition: .spring(duration: 0.4))
} else {
self.state?.updated(transition: .immediate)
}
}, },
timeoutAction: nil, timeoutAction: nil,
forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in
@ -1797,7 +1803,23 @@ public final class StoryItemSetContainerComponent: Component {
self.voiceMessagesRestrictedTooltipController = controller self.voiceMessagesRestrictedTooltipController = controller
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut))) self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}, },
paste: { _ in paste: { [weak self] data in
guard let self else {
return
}
switch data {
case let .images(images):
self.sendMessageContext.presentMediaPasteboard(view: self, subjects: images.map { .image($0) })
case let .video(data):
let tempFilePath = NSTemporaryDirectory() + "\(Int64.random(in: 0...Int64.max)).mp4"
let url = NSURL(fileURLWithPath: tempFilePath) as URL
try? data.write(to: url)
self.sendMessageContext.presentMediaPasteboard(view: self, subjects: [.video(url)])
case let .gif(data):
self.sendMessageContext.enqueueGifData(view: self, data: data)
case let .sticker(image, isMemoji):
self.sendMessageContext.enqueueStickerImage(view: self, image: image, isMemoji: isMemoji)
}
}, },
audioRecorder: self.sendMessageContext.audioRecorderValue, audioRecorder: self.sendMessageContext.audioRecorderValue,
videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil, videoRecordingStatus: !self.sendMessageContext.hasRecordedVideoPreview ? self.sendMessageContext.videoRecorderValue?.audioStatus : nil,
@ -2226,8 +2248,9 @@ public final class StoryItemSetContainerComponent: Component {
if moreButtonView.superview == nil { if moreButtonView.superview == nil {
self.controlsContainerView.addSubview(moreButtonView) self.controlsContainerView.addSubview(moreButtonView)
} }
moreButtonView.isUserInteractionEnabled = !component.slice.item.storyItem.isPending
transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize)) transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize))
transition.setAlpha(view: moreButtonView, alpha: component.slice.item.storyItem.isPending ? 0.0 : 1.0) transition.setAlpha(view: moreButtonView, alpha: component.slice.item.storyItem.isPending ? 0.5 : 1.0)
headerRightOffset -= moreButtonSize.width + 15.0 headerRightOffset -= moreButtonSize.width + 15.0
} }
@ -2811,7 +2834,24 @@ public final class StoryItemSetContainerComponent: Component {
} }
reactionContextNode.premiumReactionsSelected = { [weak self] file in reactionContextNode.premiumReactionsSelected = { [weak self] file in
guard let self, let file, let component = self.component else { guard let self, let component = self.component else {
return
}
guard let file else {
let context = component.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: {
let controller = PremiumIntroScreen(context: context, source: .reactions)
replaceImpl?(controller)
})
controller.disposed = { [weak self] in
self?.updateIsProgressPaused()
}
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
component.controller()?.push(controller)
return return
} }
@ -2827,6 +2867,9 @@ public final class StoryItemSetContainerComponent: Component {
let controller = PremiumIntroScreen(context: context, source: .reactions) let controller = PremiumIntroScreen(context: context, source: .reactions)
replaceImpl?(controller) replaceImpl?(controller)
}) })
controller.disposed = { [weak self] in
self?.updateIsProgressPaused()
}
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }
@ -2878,7 +2921,7 @@ public final class StoryItemSetContainerComponent: Component {
if let reactionContextNode = self.disappearingReactionContextNode { if let reactionContextNode = self.disappearingReactionContextNode {
if !reactionContextNode.isAnimatingOutToReaction { if !reactionContextNode.isAnimatingOutToReaction {
transition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition) reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition.containedViewLayoutTransition)
} }
} }

View File

@ -38,6 +38,8 @@ import TextFieldComponent
import StickerPackPreviewUI import StickerPackPreviewUI
import OpenInExternalAppUI import OpenInExternalAppUI
import SafariServices import SafariServices
import MediaPasteboardUI
import WebPBinding
final class StoryItemSetContainerSendMessage { final class StoryItemSetContainerSendMessage {
enum InputMode { enum InputMode {
@ -252,9 +254,12 @@ final class StoryItemSetContainerSendMessage {
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false)
let inputNodeHeight = heightAndOverflow.0 let inputNodeHeight = heightAndOverflow.0
var inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
if self.needsInputActivation { if self.needsInputActivation {
inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight)
Transition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
Transition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
} }
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame) transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
@ -326,6 +331,35 @@ final class StoryItemSetContainerSendMessage {
} }
} }
private func presentMessageSentTooltip(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messageId: EngineMessage.Id?) {
guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else {
return
}
if let tooltipScreen = self.tooltipScreen {
tooltipScreen.dismiss(animated: true)
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let tooltipScreen = UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(title: "", text: "Message Sent", cancel: messageId != nil ? "View in Chat" : "", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { [weak view, weak self] action in
if case .undo = action, let messageId {
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
self?.tooltipScreen = nil
view?.updateIsProgressPaused()
return false
}
)
controller.present(tooltipScreen, in: .current)
self.tooltipScreen = tooltipScreen
view.updateIsProgressPaused()
}
func performSendMessageAction( func performSendMessageAction(
view: StoryItemSetContainerComponent.View view: StoryItemSetContainerComponent.View
) { ) {
@ -342,8 +376,7 @@ final class StoryItemSetContainerSendMessage {
} }
let peer = component.slice.peer let peer = component.slice.peer
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let controller = component.controller() as? StoryContainerScreen
let controller = component.controller()
if let recordedAudioPreview = self.recordedAudioPreview { if let recordedAudioPreview = self.recordedAudioPreview {
self.recordedAudioPreview = nil self.recordedAudioPreview = nil
@ -370,27 +403,22 @@ final class StoryItemSetContainerSendMessage {
replyTo: nil, replyTo: nil,
storyId: focusedStoryId, storyId: focusedStoryId,
content: .text(text.string, entities) content: .text(text.string, entities)
) |> deliverOnMainQueue).start(next: { [weak controller, weak view] messageIds in ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
if let controller { Queue.mainQueue().after(0.3) {
Queue.mainQueue().after(0.3) { if let self, let view {
controller.present(UndoOverlayController( self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
presentationData: presentationData,
content: .actionSucceeded(title: "", text: "Message Sent", cancel: "View in Chat", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
), in: .current)
} }
} }
}) })
inputPanelView.clearSendMessageInput() inputPanelView.clearSendMessageInput()
self.currentInputMode = .text self.currentInputMode = .text
view.endEditing(true) if hasFirstResponder(view) {
view.endEditing(true)
} else {
view.state?.updated(transition: .spring(duration: 0.3))
}
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
} }
} }
} }
@ -407,7 +435,6 @@ final class StoryItemSetContainerSendMessage {
let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id)
let peer = component.slice.peer let peer = component.slice.peer
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let controller = component.controller() as? StoryContainerScreen let controller = component.controller() as? StoryContainerScreen
if let navigationController = controller?.navigationController as? NavigationController { if let navigationController = controller?.navigationController as? NavigationController {
@ -433,21 +460,10 @@ final class StoryItemSetContainerSendMessage {
replyTo: nil, replyTo: nil,
storyId: focusedStoryId, storyId: focusedStoryId,
content: .file(fileReference) content: .file(fileReference)
) |> deliverOnMainQueue).start(next: { [weak controller, weak view] messageIds in ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
if let controller { Queue.mainQueue().after(0.3) {
Queue.mainQueue().after(0.3) { if let self, let view {
controller.present(UndoOverlayController( self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
presentationData: presentationData,
content: .actionSucceeded(title: "", text: "Message Sent", cancel: "View in Chat", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
), in: .current)
} }
} }
}) })
@ -457,8 +473,8 @@ final class StoryItemSetContainerSendMessage {
view.endEditing(true) view.endEditing(true)
} else { } else {
view.state?.updated(transition: .spring(duration: 0.3)) view.state?.updated(transition: .spring(duration: 0.3))
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
} }
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
} }
func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult) { func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult) {
@ -472,7 +488,6 @@ final class StoryItemSetContainerSendMessage {
let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id)
let peer = component.slice.peer let peer = component.slice.peer
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let controller = component.controller() as? StoryContainerScreen let controller = component.controller() as? StoryContainerScreen
if let navigationController = controller?.navigationController as? NavigationController { if let navigationController = controller?.navigationController as? NavigationController {
@ -498,21 +513,10 @@ final class StoryItemSetContainerSendMessage {
replyTo: nil, replyTo: nil,
storyId: focusedStoryId, storyId: focusedStoryId,
content: .contextResult(results, result) content: .contextResult(results, result)
) |> deliverOnMainQueue).start(next: { [weak controller, weak view] messageIds in ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
if let controller { Queue.mainQueue().after(0.3) {
Queue.mainQueue().after(0.3) { if let self, let view {
controller.present(UndoOverlayController( self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
presentationData: presentationData,
content: .actionSucceeded(title: "", text: "Message Sent", cancel: "View in Chat", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
), in: .current)
} }
} }
}) })
@ -522,8 +526,75 @@ final class StoryItemSetContainerSendMessage {
view.endEditing(true) view.endEditing(true)
} else { } else {
view.state?.updated(transition: .spring(duration: 0.3)) view.state?.updated(transition: .spring(duration: 0.3))
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
} }
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
}
func enqueueGifData(view: StoryItemSetContainerComponent.View, data: Data) {
guard let component = view.component else {
return
}
let peer = component.slice.peer
let _ = (legacyEnqueueGifMessage(account: component.context.account, data: data) |> deliverOnMainQueue).start(next: { [weak self, weak view] message in
if let self, let view {
self.sendMessages(view: view, peer: peer, messages: [message])
}
})
}
func enqueueStickerImage(view: StoryItemSetContainerComponent.View, image: UIImage, isMemoji: Bool) {
guard let component = view.component else {
return
}
let peer = component.slice.peer
let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0))
func scaleImage(_ image: UIImage, size: CGSize, boundiingSize: CGSize) -> UIImage? {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
let renderer = UIGraphicsImageRenderer(size: size, format: format)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: size))
}
} else {
return TGScaleImageToPixelSize(image, size)
}
}
func convertToWebP(image: UIImage, targetSize: CGSize?, targetBoundingSize: CGSize?, quality: CGFloat) -> Signal<Data, NoError> {
var image = image
if let targetSize = targetSize, let scaledImage = scaleImage(image, size: targetSize, boundiingSize: targetSize) {
image = scaledImage
}
return Signal { subscriber in
if let data = try? WebP.convert(toWebP: image, quality: quality * 100.0) {
subscriber.putNext(data)
}
subscriber.putCompletion()
return EmptyDisposable
} |> runOn(Queue.concurrentDefaultQueue())
}
let _ = (convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).start(next: { [weak self, weak view] data in
if let self, let view, !data.isEmpty {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.FileName(fileName: "sticker.webp"))
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
fileAttributes.append(.ImageSize(size: PixelDimensions(size)))
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes)
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
self.sendMessages(view: view, peer: peer, messages: [message], silentPosting: false)
}
})
} }
func setMediaRecordingActive( func setMediaRecordingActive(
@ -1160,18 +1231,13 @@ final class StoryItemSetContainerSendMessage {
} }
let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: mediaReference, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: [message.withUpdatedReplyToMessageId(nil)]) let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: [message.withUpdatedReplyToMessageId(nil)])
|> deliverOnMainQueue).start() |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
if let self, let view {
if let controller = component.controller() { Queue.mainQueue().after(0.3) {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
controller.present(UndoOverlayController( }
presentationData: presentationData, }
content: .succeed(text: "Message Sent"), })
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
}) })
let _ = currentFilesController.swap(controller) let _ = currentFilesController.swap(controller)
if let controller = controller as? AttachmentContainable, let mediaPickerContext = controller.mediaPickerContext { if let controller = controller as? AttachmentContainable, let mediaPickerContext = controller.mediaPickerContext {
@ -1275,7 +1341,7 @@ final class StoryItemSetContainerSendMessage {
} }
} }
self.sendMessages(view: view, peer: peer, messages: self.transformEnqueueMessages(view: view, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) self.sendMessages(view: view, peer: peer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)
} else if let peer = peers.first { } else if let peer = peers.first {
let dataSignal: Signal<(EnginePeer?, DeviceContactExtendedData?), NoError> let dataSignal: Signal<(EnginePeer?, DeviceContactExtendedData?), NoError>
switch peer { switch peer {
@ -1330,7 +1396,7 @@ final class StoryItemSetContainerSendMessage {
} }
enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
self.sendMessages(view: view, peer: targetPeer, messages: self.transformEnqueueMessages(view: view, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)
} else { } else {
let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: component.context, subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: component.context, subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in
guard let self, let view else { guard let self, let view else {
@ -1349,7 +1415,7 @@ final class StoryItemSetContainerSendMessage {
} }
enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))
self.sendMessages(view: view, peer: targetPeer, messages: self.transformEnqueueMessages(view: view, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)) self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)
} }
}), completed: nil, cancelled: nil) }), completed: nil, cancelled: nil)
component.controller()?.push(contactController) component.controller()?.push(contactController)
@ -1357,10 +1423,6 @@ final class StoryItemSetContainerSendMessage {
})) }))
} }
})) }))
case .poll:
let controller = self.configurePollCreation(view: view, peer: peer, targetMessageId: nil)
completion(controller, controller?.mediaPickerContext)
self.controllerNavigationDisposable.set(nil)
case .gift: case .gift:
/*let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions /*let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions
if !premiumGiftOptions.isEmpty { if !premiumGiftOptions.isEmpty {
@ -1773,6 +1835,69 @@ final class StoryItemSetContainerSendMessage {
}) })
} }
func presentMediaPasteboard(view: StoryItemSetContainerComponent.View, subjects: [MediaPickerScreen.Subject.Media]) {
guard let component = view.component else {
return
}
let focusedItem = component.slice.item
guard let peerId = focusedItem.peerId else {
return
}
let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id)
guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
var inputText = NSAttributedString(string: "")
switch inputPanelView.getSendMessageInput() {
case let .text(text):
inputText = text
}
let peer = component.slice.peer
let theme = defaultDarkPresentationTheme
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
let controller = mediaPasteboardScreen(
context: component.context,
updatedPresentationData: updatedPresentationData,
peer: peer,
subjects: subjects,
presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in
if let self {
self.presentMediaPicker(
view: view,
peer: peer,
replyToMessageId: nil,
replyToStoryId: focusedStoryId,
subject: subject,
saveEditedPhotos: saveEditedPhotos,
bannedSendPhotos: bannedSendPhotos,
bannedSendVideos: bannedSendVideos,
present: { controller, mediaPickerContext in
if !inputText.string.isEmpty {
mediaPickerContext?.setCaption(inputText)
}
present(controller, mediaPickerContext)
},
updateMediaPickerContext: { _ in },
completion: { [weak self, weak view] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
guard let self, let view else {
return
}
if !inputText.string.isEmpty {
self.clearInputText(view: view)
}
self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: focusedStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion)
}
)
}
},
getSourceRect: nil
)
controller.navigationPresentation = .flatModal
component.controller()?.push(controller)
}
private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) {
if !canSendMessagesToPeer(peer._asPeer()) { if !canSendMessagesToPeer(peer._asPeer()) {
return return
@ -2002,56 +2127,6 @@ final class StoryItemSetContainerSendMessage {
component.controller()?.present(controller, in: .window(.root)) component.controller()?.present(controller, in: .window(.root))
} }
private func configurePollCreation(view: StoryItemSetContainerComponent.View, peer: EnginePeer, targetMessageId: EngineMessage.Id?, isQuiz: Bool? = nil) -> CreatePollControllerImpl? {
guard let component = view.component else {
return nil
}
let focusedItem = component.slice.item
guard let peerId = focusedItem.peerId else {
return nil
}
let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id)
let theme = component.theme
return createPollController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), peer: peer, isQuiz: isQuiz, completion: { [weak self, weak view] poll in
guard let self, let view else {
return
}
let replyMessageId = targetMessageId
/*strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
}, nil)*/
let message: EnqueueMessage = .message(
text: "",
attributes: [],
inlineStickers: [:],
mediaReference: .standalone(media: TelegramMediaPoll(
pollId: EngineMedia.Id(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
publicity: poll.publicity,
kind: poll.kind,
text: poll.text,
options: poll.options,
correctAnswers: poll.correctAnswers,
results: poll.results,
isClosed: false,
deadlineTimeout: poll.deadlineTimeout
)),
replyToMessageId: nil,
replyToStoryId: focusedStoryId,
localGroupingKey: nil,
correlationId: nil,
bubbleUpEmojiOrStickersets: []
)
self.sendMessages(view: view, peer: peer, messages: [message.withUpdatedReplyToMessageId(replyMessageId)])
})
}
private func transformEnqueueMessages(view: StoryItemSetContainerComponent.View, messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { private func transformEnqueueMessages(view: StoryItemSetContainerComponent.View, messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] {
var focusedStoryId: StoryId? var focusedStoryId: StoryId?
if let component = view.component, let peerId = component.slice.item.peerId { if let component = view.component, let peerId = component.slice.item.peerId {
@ -2094,28 +2169,15 @@ final class StoryItemSetContainerSendMessage {
} }
} }
private func sendMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { private func sendMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messages: [EnqueueMessage], silentPosting: Bool = false, scheduleTime: Int32? = nil) {
guard let component = view.component, let controller = component.controller() else { guard let component = view.component else {
return return
} }
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: self.transformEnqueueMessages(view: view, messages: messages, silentPosting: silentPosting, scheduleTime: scheduleTime))
|> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
let _ = (enqueueMessages(account: component.context.account, peerId: peer.id, messages: self.transformEnqueueMessages(view: view, messages: messages, silentPosting: false)) Queue.mainQueue().after(0.3) {
|> deliverOnMainQueue).start(next: { [weak controller] messageIds in if let view {
if let controller { self?.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
Queue.mainQueue().after(0.3) {
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(title: "", text: "Message Sent", cancel: "View in Chat", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
), in: .current)
} }
} }
}) })
@ -2180,10 +2242,8 @@ final class StoryItemSetContainerSendMessage {
} }
mappedMessages.append(message) mappedMessages.append(message)
} }
let messages = strongSelf.transformEnqueueMessages(view: view, messages: mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime)
strongSelf.sendMessages(view: view, peer: peer, messages: messages.map { $0.withUpdatedReplyToMessageId(replyToMessageId).withUpdatedReplyToStoryId(replyToStoryId) }, media: true) strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime)
completion() completion()
} }

File diff suppressed because one or more lines are too long