mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various fixes
This commit is contained in:
parent
0b7869688d
commit
5e5f19e6e9
@ -82,6 +82,16 @@ public final class DeviceAccess {
|
||||
return self.locationPromise.get()
|
||||
}
|
||||
|
||||
private static let cameraPromise = Promise<Bool?>(nil)
|
||||
static var camera: Signal<Bool?, NoError> {
|
||||
return self.cameraPromise.get()
|
||||
}
|
||||
|
||||
private static let microphonePromise = Promise<Bool?>(nil)
|
||||
static var microphone: Signal<Bool?, NoError> {
|
||||
return self.microphonePromise.get()
|
||||
}
|
||||
|
||||
public static func isMicrophoneAccessAuthorized() -> Bool? {
|
||||
return AVAudioSession.sharedInstance().recordPermission == .granted
|
||||
}
|
||||
@ -248,12 +258,72 @@ public final class DeviceAccess {
|
||||
}
|
||||
}
|
||||
)
|
||||
case .camera:
|
||||
return Signal { subscriber in
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
switch status {
|
||||
case .authorized:
|
||||
subscriber.putNext(.allowed)
|
||||
case .denied, .restricted:
|
||||
subscriber.putNext(.denied)
|
||||
case .notDetermined:
|
||||
subscriber.putNext(.notDetermined)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
}
|
||||
|> then(self.camera
|
||||
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
|
||||
if let authorized = authorized {
|
||||
return .single(authorized ? .allowed : .denied)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
)
|
||||
case .microphone:
|
||||
return Signal { subscriber in
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||
switch status {
|
||||
case .authorized:
|
||||
subscriber.putNext(.allowed)
|
||||
case .denied, .restricted:
|
||||
subscriber.putNext(.denied)
|
||||
case .notDetermined:
|
||||
subscriber.putNext(.notDetermined)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
}
|
||||
|> then(self.microphone
|
||||
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
|
||||
if let authorized = authorized {
|
||||
return .single(authorized ? .allowed : .denied)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
)
|
||||
default:
|
||||
return .single(.notDetermined)
|
||||
}
|
||||
}
|
||||
|
||||
public static func authorizeAccess(to subject: DeviceAccessSubject, onlyCheck: Bool = false, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: LocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
public static func authorizeAccess(
|
||||
to subject: DeviceAccessSubject,
|
||||
onlyCheck: Bool = false,
|
||||
registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil,
|
||||
requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil,
|
||||
locationManager: LocationManager? = nil,
|
||||
presentationData: PresentationData? = nil,
|
||||
present: @escaping (ViewController, Any?) -> Void = { _, _ in },
|
||||
openSettings: @escaping () -> Void = { },
|
||||
displayNotificationFromBackground: @escaping (String) -> Void = { _ in },
|
||||
_ completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
switch subject {
|
||||
case let .camera(cameraSubject):
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
@ -262,6 +332,7 @@ public final class DeviceAccess {
|
||||
AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in
|
||||
Queue.mainQueue().async {
|
||||
completion(response)
|
||||
self.cameraPromise.set(.single(response))
|
||||
if !response, let presentationData = presentationData {
|
||||
let text: String
|
||||
switch cameraSubject {
|
||||
@ -331,6 +402,7 @@ public final class DeviceAccess {
|
||||
displayNotificationFromBackground(text)
|
||||
}
|
||||
}
|
||||
self.microphonePromise.set(.single(granted))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -694,6 +694,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView {
|
||||
if !self.hasBin {
|
||||
selectionView.handlePan(gestureRecognizer)
|
||||
} else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .dualVideoReference = stickerEntity.content {
|
||||
selectionView.handlePan(gestureRecognizer)
|
||||
} else {
|
||||
var isTrappedInBin = false
|
||||
let scale = 100.0 / selectedEntityView.bounds.size.width
|
||||
|
@ -2930,7 +2930,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
}
|
||||
let images = imageItems as! [UIImage]
|
||||
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
|
||||
let entity = DrawingStickerEntity(content: .image(image, false))
|
||||
let entity = DrawingStickerEntity(content: .image(image, .sticker))
|
||||
strongSelf.node.insertEntity.invoke(entity)
|
||||
}
|
||||
}
|
||||
|
@ -585,7 +585,7 @@ public class StickerPickerScreen: ViewController {
|
||||
CTLineDraw(line, context)
|
||||
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
|
||||
}) {
|
||||
strongSelf.controller?.completion(.image(image, false))
|
||||
strongSelf.controller?.completion(.image(image, .sticker))
|
||||
}
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
}
|
||||
|
@ -71,12 +71,16 @@ swift_library(
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/Components/MetalImageView",
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
"//submodules/Utils/VolumeButtons",
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/DeviceAccess",
|
||||
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,6 +21,7 @@ import BundleIconComponent
|
||||
import CameraButtonComponent
|
||||
import VolumeButtons
|
||||
import TelegramNotices
|
||||
import DeviceAccess
|
||||
|
||||
let videoRedColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
@ -87,6 +88,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
|
||||
let context: AccountContext
|
||||
let cameraState: CameraState
|
||||
let cameraAuthorizationStatus: AccessType
|
||||
let microphoneAuthorizationStatus: AccessType
|
||||
let hasAppeared: Bool
|
||||
let isVisible: Bool
|
||||
let panelWidth: CGFloat
|
||||
@ -101,6 +104,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
init(
|
||||
context: AccountContext,
|
||||
cameraState: CameraState,
|
||||
cameraAuthorizationStatus: AccessType,
|
||||
microphoneAuthorizationStatus: AccessType,
|
||||
hasAppeared: Bool,
|
||||
isVisible: Bool,
|
||||
panelWidth: CGFloat,
|
||||
@ -114,6 +119,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
) {
|
||||
self.context = context
|
||||
self.cameraState = cameraState
|
||||
self.cameraAuthorizationStatus = cameraAuthorizationStatus
|
||||
self.microphoneAuthorizationStatus = microphoneAuthorizationStatus
|
||||
self.hasAppeared = hasAppeared
|
||||
self.isVisible = isVisible
|
||||
self.panelWidth = panelWidth
|
||||
@ -133,6 +140,12 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if lhs.cameraState != rhs.cameraState {
|
||||
return false
|
||||
}
|
||||
if lhs.cameraAuthorizationStatus != rhs.cameraAuthorizationStatus {
|
||||
return false
|
||||
}
|
||||
if lhs.microphoneAuthorizationStatus != rhs.microphoneAuthorizationStatus {
|
||||
return false
|
||||
}
|
||||
if lhs.hasAppeared != rhs.hasAppeared {
|
||||
return false
|
||||
}
|
||||
@ -166,11 +179,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
|
||||
private var microphoneAuthorizationStatus: AVAuthorizationStatus = .notDetermined
|
||||
private var galleryAuthorizationStatus: PHAuthorizationStatus = .notDetermined
|
||||
|
||||
|
||||
private let context: AccountContext
|
||||
private let present: (ViewController) -> Void
|
||||
private let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@ -180,7 +189,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
private let getController: () -> CameraScreen?
|
||||
|
||||
private var resultDisposable = MetaDisposable()
|
||||
|
||||
|
||||
private var mediaAssetsContext: MediaAssetsContext?
|
||||
fileprivate var lastGalleryAsset: PHAsset?
|
||||
private var lastGalleryAssetsDisposable: Disposable?
|
||||
@ -502,6 +511,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let placeholder = Child(PlaceholderComponent.self)
|
||||
let cancelButton = Child(CameraButton.self)
|
||||
let captureControls = Child(CaptureControlsComponent.self)
|
||||
let zoomControl = Child(ZoomComponent.self)
|
||||
@ -535,12 +545,51 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let panelWidth = min(component.panelWidth, 185.0)
|
||||
|
||||
var controlsBottomInset: CGFloat = 0.0
|
||||
let previewHeight = floorToScreenPixels(availableSize.width * 1.77778)
|
||||
if !isTablet {
|
||||
let previewHeight = floorToScreenPixels(availableSize.width * 1.77778)
|
||||
if availableSize.height < previewHeight + 30.0 {
|
||||
controlsBottomInset = -48.0
|
||||
}
|
||||
}
|
||||
|
||||
let hasAllRequiredAccess: Bool
|
||||
switch component.cameraAuthorizationStatus {
|
||||
case .notDetermined:
|
||||
hasAllRequiredAccess = true
|
||||
case .allowed:
|
||||
switch component.microphoneAuthorizationStatus {
|
||||
case .notDetermined:
|
||||
hasAllRequiredAccess = true
|
||||
case .allowed:
|
||||
hasAllRequiredAccess = true
|
||||
default:
|
||||
hasAllRequiredAccess = false
|
||||
}
|
||||
default:
|
||||
hasAllRequiredAccess = false
|
||||
}
|
||||
|
||||
if !hasAllRequiredAccess {
|
||||
let accountContext = component.context
|
||||
let placeholder = placeholder.update(
|
||||
component: PlaceholderComponent(
|
||||
context: component.context,
|
||||
mode: .denied,
|
||||
action: {
|
||||
accountContext.sharedContext.applicationBindings.openSettings()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: availableSize.width, height: previewHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(placeholder
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + previewHeight / 2.0))
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(11.0)
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
if case .holding = component.cameraState.recording {
|
||||
|
||||
@ -593,7 +642,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let captureControls = captureControls.update(
|
||||
component: CaptureControlsComponent(
|
||||
isTablet: isTablet,
|
||||
hasAppeared: component.hasAppeared,
|
||||
hasAppeared: component.hasAppeared && hasAllRequiredAccess,
|
||||
hasAccess: hasAllRequiredAccess,
|
||||
shutterState: shutterState,
|
||||
lastGalleryAsset: state.lastGalleryAsset,
|
||||
tag: captureControlsTag,
|
||||
@ -739,51 +789,53 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
)
|
||||
}
|
||||
|
||||
let flashButton = flashButton.update(
|
||||
component: CameraButton(
|
||||
content: flashContentComponent,
|
||||
action: { [weak state] in
|
||||
if let state {
|
||||
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.isDualCameraSupported {
|
||||
let dualButton = dualButton.update(
|
||||
if hasAllRequiredAccess {
|
||||
let flashButton = flashButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "dual",
|
||||
component: AnyComponent(
|
||||
DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled)
|
||||
)
|
||||
),
|
||||
content: flashContentComponent,
|
||||
action: { [weak state] in
|
||||
if let state {
|
||||
state.toggleDualCamera()
|
||||
state.toggleFlashMode()
|
||||
}
|
||||
}
|
||||
).tagged(dualButtonTag),
|
||||
).tagged(flashButtonTag),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(dualButton
|
||||
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 58.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 2.0))
|
||||
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.isDualCameraSupported {
|
||||
let dualButton = dualButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "dual",
|
||||
component: AnyComponent(
|
||||
DualIconComponent(isSelected: component.cameraState.isDualCameraEnabled)
|
||||
)
|
||||
),
|
||||
action: { [weak state] in
|
||||
if let state {
|
||||
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 - 58.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 2.0))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isTablet {
|
||||
if isTablet && hasAllRequiredAccess {
|
||||
let flipButton = flipButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
@ -807,6 +859,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
)
|
||||
context.add(flipButton
|
||||
.position(CGPoint(x: smallPanelWidth / 2.0, y: availableSize.height / 2.0))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
}
|
||||
|
||||
@ -885,7 +939,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if case .none = component.cameraState.recording, !state.isTransitioning {
|
||||
if case .none = component.cameraState.recording, !state.isTransitioning && hasAllRequiredAccess {
|
||||
let availableModeControlSize: CGSize
|
||||
if isTablet {
|
||||
availableModeControlSize = CGSize(width: panelWidth, height: 120.0)
|
||||
@ -1141,6 +1195,11 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var cameraAuthorizationStatus: AccessType = .notDetermined
|
||||
private var microphoneAuthorizationStatus: AccessType = .notDetermined
|
||||
private var galleryAuthorizationStatus: AccessType = .notDetermined
|
||||
private var authorizationStatusDisposables = DisposableSet()
|
||||
|
||||
init(controller: CameraScreen) {
|
||||
self.controller = controller
|
||||
@ -1312,12 +1371,33 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
|
||||
|
||||
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let self {
|
||||
self.cameraAuthorizationStatus = status
|
||||
self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .easeInOut(duration: 0.2))
|
||||
|
||||
self.maybeSetupCamera()
|
||||
}
|
||||
}))
|
||||
|
||||
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let self {
|
||||
self.microphoneAuthorizationStatus = status
|
||||
self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .easeInOut(duration: 0.2))
|
||||
|
||||
self.maybeSetupCamera()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.cameraStateDisposable?.dispose()
|
||||
self.changingPositionDisposable?.dispose()
|
||||
self.idleTimerExtensionDisposable.dispose()
|
||||
self.authorizationStatusDisposables.dispose()
|
||||
}
|
||||
|
||||
private var pipPanGestureRecognizer: UIPanGestureRecognizer?
|
||||
@ -1346,11 +1426,23 @@ public class CameraScreen: ViewController {
|
||||
pipPanGestureRecognizer.delegate = self
|
||||
self.previewContainerView.addGestureRecognizer(pipPanGestureRecognizer)
|
||||
self.pipPanGestureRecognizer = pipPanGestureRecognizer
|
||||
|
||||
self.setupCamera()
|
||||
}
|
||||
|
||||
func setupCamera() {
|
||||
private func maybeSetupCamera() {
|
||||
if case .allowed = self.cameraAuthorizationStatus, case .allowed = self.microphoneAuthorizationStatus {
|
||||
self.setupCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private func requestDeviceAccess() {
|
||||
DeviceAccess.authorizeAccess(to: .camera(.video), { granted in
|
||||
if granted {
|
||||
DeviceAccess.authorizeAccess(to: .microphone(.video))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func setupCamera() {
|
||||
guard self.camera == nil else {
|
||||
return
|
||||
}
|
||||
@ -1461,6 +1553,10 @@ public class CameraScreen: ViewController {
|
||||
camera.startCapture()
|
||||
|
||||
self.camera = camera
|
||||
|
||||
if self.hasAppeared {
|
||||
self.maybePresentTooltips()
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
@ -1581,7 +1677,7 @@ public class CameraScreen: ViewController {
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.requestUpdateLayout(hasAppeared: false, transition: .immediate)
|
||||
self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate)
|
||||
CATransaction.commit()
|
||||
|
||||
self.additionalPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
@ -1866,9 +1962,12 @@ public class CameraScreen: ViewController {
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 3.0), size: CGSize())
|
||||
|
||||
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(4.5), 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: { [weak self] point, containerFrame in
|
||||
if containerFrame.contains(point) {
|
||||
let _ = ApplicationSpecificNotice.incrementStoriesCameraTip(accountManager: accountManager).start()
|
||||
Queue.mainQueue().justDispatch {
|
||||
self?.maybePresentTooltips()
|
||||
}
|
||||
return .dismiss(consume: true)
|
||||
}
|
||||
return .ignore
|
||||
@ -2006,7 +2105,11 @@ public class CameraScreen: ViewController {
|
||||
self.hasAppeared = hasAppeared
|
||||
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
|
||||
|
||||
self.maybePresentTooltips()
|
||||
if self.camera != nil {
|
||||
self.maybePresentTooltips()
|
||||
} else if case .notDetermined = self.cameraAuthorizationStatus {
|
||||
self.requestDeviceAccess()
|
||||
}
|
||||
}
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
@ -2015,6 +2118,8 @@ public class CameraScreen: ViewController {
|
||||
CameraScreenComponent(
|
||||
context: self.context,
|
||||
cameraState: self.cameraState,
|
||||
cameraAuthorizationStatus: self.cameraAuthorizationStatus,
|
||||
microphoneAuthorizationStatus: self.microphoneAuthorizationStatus,
|
||||
hasAppeared: self.hasAppeared,
|
||||
isVisible: self.cameraIsActive && !self.hasGallery,
|
||||
panelWidth: panelWidth,
|
||||
|
@ -446,6 +446,7 @@ final class CaptureControlsComponent: Component {
|
||||
|
||||
let isTablet: Bool
|
||||
let hasAppeared: Bool
|
||||
let hasAccess: Bool
|
||||
let shutterState: ShutterButtonState
|
||||
let lastGalleryAsset: PHAsset?
|
||||
let tag: AnyObject?
|
||||
@ -463,6 +464,7 @@ final class CaptureControlsComponent: Component {
|
||||
init(
|
||||
isTablet: Bool,
|
||||
hasAppeared: Bool,
|
||||
hasAccess: Bool,
|
||||
shutterState: ShutterButtonState,
|
||||
lastGalleryAsset: PHAsset?,
|
||||
tag: AnyObject?,
|
||||
@ -479,6 +481,7 @@ final class CaptureControlsComponent: Component {
|
||||
) {
|
||||
self.isTablet = isTablet
|
||||
self.hasAppeared = hasAppeared
|
||||
self.hasAccess = hasAccess
|
||||
self.shutterState = shutterState
|
||||
self.lastGalleryAsset = lastGalleryAsset
|
||||
self.tag = tag
|
||||
@ -501,6 +504,9 @@ final class CaptureControlsComponent: Component {
|
||||
if lhs.hasAppeared != rhs.hasAppeared {
|
||||
return false
|
||||
}
|
||||
if lhs.hasAccess != rhs.hasAccess {
|
||||
return false
|
||||
}
|
||||
if lhs.shutterState != rhs.shutterState {
|
||||
return false
|
||||
}
|
||||
@ -944,7 +950,7 @@ final class CaptureControlsComponent: Component {
|
||||
transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if !component.isTablet {
|
||||
if !component.isTablet && component.hasAccess {
|
||||
let flipButtonOriginX = availableSize.width - 48.0 - buttonSideInset
|
||||
let flipButtonMaskFrame: CGRect = CGRect(origin: CGPoint(x: availableSize.width / 2.0 - (flipButtonOriginX + 22.0) + 6.0 + self.shutterOffsetX, y: 8.0), size: CGSize(width: 32.0, height: 32.0))
|
||||
|
||||
@ -1153,10 +1159,13 @@ final class CaptureControlsComponent: Component {
|
||||
|
||||
self.addSubview(shutterButtonView)
|
||||
}
|
||||
let alpha: CGFloat = component.hasAccess ? 1.0 : 0.3
|
||||
transition.setBounds(view: shutterButtonView, bounds: CGRect(origin: .zero, size: shutterButtonFrame.size))
|
||||
transition.setPosition(view: shutterButtonView, position: shutterButtonFrame.center)
|
||||
transition.setScale(view: shutterButtonView, scale: isTransitioning ? 0.01 : 1.0)
|
||||
transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : alpha)
|
||||
|
||||
shutterButtonView.isUserInteractionEnabled = component.hasAccess
|
||||
}
|
||||
|
||||
if let buttonView = self.flipButtonView.view as? CameraButton.View, let contentView = buttonView.contentView.componentView as? FlipButtonContentComponent.View {
|
||||
|
@ -0,0 +1,224 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import ButtonComponent
|
||||
import LottieComponent
|
||||
|
||||
final class PlaceholderComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
enum Mode {
|
||||
case request
|
||||
case denied
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let mode: Mode
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
mode: Mode,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: PlaceholderComponent, rhs: PlaceholderComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.mode != rhs.mode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let animation = ComponentView<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var component: PlaceholderComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||
// if #available(iOS 13.0, *) {
|
||||
// self.layer.cornerCurve = .continuous
|
||||
// }
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: PlaceholderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 36.0
|
||||
let animationHeight: CGFloat = 120.0
|
||||
|
||||
let title: String = "Allow Telegram to access your camera and microphone"
|
||||
let text: String = "This lets you share photos and record videos."
|
||||
let buttonTitle: String = "Open Settings"
|
||||
|
||||
let animationSize = self.animation.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "Photos")
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: animationHeight, height: animationHeight)
|
||||
)
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: UIColor.white)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 3.0, height: availableSize.height)
|
||||
)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor(rgb: 0x98989f))),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: UIColor(rgb: 0x007aff),
|
||||
foreground: .white,
|
||||
pressedColor: UIColor(rgb: 0x007aff, alpha: 0.55)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: buttonTitle,
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: buttonTitle,
|
||||
badge: 0,
|
||||
textColor: .white,
|
||||
badgeBackground: .clear,
|
||||
badgeForeground: .clear
|
||||
))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.component?.action()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 240.0, height: 50.0)
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 12.0
|
||||
let textSpacing: CGFloat = 14.0
|
||||
let buttonSpacing: CGFloat = 18.0
|
||||
let totalHeight = animationSize.height + titleSpacing + titleSize.height + textSpacing + textSize.height + buttonSpacing + buttonSize.height
|
||||
|
||||
var originY = floorToScreenPixels((availableSize.height - totalHeight) / 2.0)
|
||||
let animationFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: floorToScreenPixels((availableSize.width - animationSize.width) / 2.0),
|
||||
y: originY
|
||||
),
|
||||
size: animationSize
|
||||
)
|
||||
if let view = self.animation.view as? LottieComponent.View {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
Queue.mainQueue().justDispatch {
|
||||
view.playOnce()
|
||||
}
|
||||
}
|
||||
view.frame = animationFrame
|
||||
}
|
||||
originY += animationSize.height + titleSpacing
|
||||
|
||||
let titleFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0),
|
||||
y: originY
|
||||
),
|
||||
size: titleSize
|
||||
)
|
||||
if let view = self.title.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = titleFrame
|
||||
}
|
||||
originY += titleSize.height + textSpacing
|
||||
|
||||
let textFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: floorToScreenPixels((availableSize.width - textSize.width) / 2.0),
|
||||
y: originY
|
||||
),
|
||||
size: textSize
|
||||
)
|
||||
if let view = self.text.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = textFrame
|
||||
}
|
||||
originY += textSize.height + buttonSpacing
|
||||
|
||||
let buttonFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0),
|
||||
y: originY
|
||||
),
|
||||
size: buttonSize
|
||||
)
|
||||
if let view = self.button.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = buttonFrame
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -14,8 +14,13 @@ private func fullEntityMediaPath(_ path: String) -> String {
|
||||
|
||||
public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
public enum Content: Equatable {
|
||||
public enum ImageType: Equatable {
|
||||
case sticker
|
||||
case rectangle
|
||||
case dualPhoto
|
||||
}
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage, Bool)
|
||||
case image(UIImage, ImageType)
|
||||
case video(String, UIImage?, Bool)
|
||||
case dualVideoReference
|
||||
|
||||
@ -27,9 +32,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .image(lhsImage, lhsIsRectangle):
|
||||
if case let .image(rhsImage, rhsIsRectangle) = rhs {
|
||||
return lhsImage === rhsImage && lhsIsRectangle == rhsIsRectangle
|
||||
case let .image(lhsImage, lhsImageType):
|
||||
if case let .image(rhsImage, rhsImageType) = rhs {
|
||||
return lhsImage === rhsImage && lhsImageType == rhsImageType
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@ -56,6 +61,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
case videoImagePath
|
||||
case videoMirrored
|
||||
case isRectangle
|
||||
case isDualPhoto
|
||||
case dualVideo
|
||||
case referenceDrawingSize
|
||||
case position
|
||||
@ -121,8 +127,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
|
||||
public var isRectangle: Bool {
|
||||
switch self.content {
|
||||
case let .image(_, isRectangle):
|
||||
return isRectangle
|
||||
case let .image(_, imageType):
|
||||
return imageType == .rectangle
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@ -157,7 +163,16 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
self.content = .file(file)
|
||||
} else if let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) {
|
||||
let isRectangle = try container.decodeIfPresent(Bool.self, forKey: .isRectangle) ?? false
|
||||
self.content = .image(image, isRectangle)
|
||||
let isDualPhoto = try container.decodeIfPresent(Bool.self, forKey: .isDualPhoto) ?? false
|
||||
let imageType: Content.ImageType
|
||||
if isDualPhoto {
|
||||
imageType = .dualPhoto
|
||||
} else if isRectangle {
|
||||
imageType = .rectangle
|
||||
} else {
|
||||
imageType = .sticker
|
||||
}
|
||||
self.content = .image(image, imageType)
|
||||
} else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) {
|
||||
var imageValue: UIImage?
|
||||
if let imagePath = try container.decodeIfPresent(String.self, forKey: .videoImagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) {
|
||||
@ -182,7 +197,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
switch self.content {
|
||||
case let .file(file):
|
||||
try container.encode(file, forKey: .file)
|
||||
case let .image(image, isRectangle):
|
||||
case let .image(image, imageType):
|
||||
let imagePath = "\(self.uuid).png"
|
||||
let fullImagePath = fullEntityMediaPath(imagePath)
|
||||
if let imageData = image.pngData() {
|
||||
@ -190,7 +205,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
try? imageData.write(to: URL(fileURLWithPath: fullImagePath))
|
||||
try container.encodeIfPresent(imagePath, forKey: .imagePath)
|
||||
}
|
||||
try container.encode(isRectangle, forKey: .isRectangle)
|
||||
switch imageType {
|
||||
case .dualPhoto:
|
||||
try container.encode(true, forKey: .isDualPhoto)
|
||||
case .rectangle:
|
||||
try container.encode(true, forKey: .isRectangle)
|
||||
default:
|
||||
break
|
||||
}
|
||||
case let .video(path, image, videoMirrored):
|
||||
try container.encode(path, forKey: .videoPath)
|
||||
let imagePath = "\(self.uuid).jpg"
|
||||
|
@ -481,7 +481,8 @@ public final class MediaEditor {
|
||||
|
||||
if player == nil {
|
||||
self.updateRenderChain()
|
||||
self.maybeGeneratePersonSegmentation(image)
|
||||
let _ = image
|
||||
// self.maybeGeneratePersonSegmentation(image)
|
||||
}
|
||||
|
||||
if let player {
|
||||
|
@ -1167,13 +1167,13 @@ final class MediaEditorScreenComponent: Component {
|
||||
switch data {
|
||||
case let .sticker(image, _):
|
||||
if max(image.size.width, image.size.height) > 1.0 {
|
||||
let entity = DrawingStickerEntity(content: .image(image, false))
|
||||
let entity = DrawingStickerEntity(content: .image(image, .sticker))
|
||||
controller.node.interaction?.insertEntity(entity, scale: 1.0)
|
||||
self.deactivateInput()
|
||||
}
|
||||
case let .images(images):
|
||||
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
|
||||
let entity = DrawingStickerEntity(content: .image(image, true))
|
||||
let entity = DrawingStickerEntity(content: .image(image, .rectangle))
|
||||
controller.node.interaction?.insertEntity(entity, scale: 2.5)
|
||||
self.deactivateInput()
|
||||
}
|
||||
@ -1925,8 +1925,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let cgImage = additionalImage.cgImage {
|
||||
context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size))
|
||||
}
|
||||
})
|
||||
let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, false))
|
||||
}, scale: 1.0)
|
||||
let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage, .dualPhoto))
|
||||
imageEntity.referenceDrawingSize = storyDimensions
|
||||
imageEntity.scale = 1.625
|
||||
imageEntity.position = position.getPosition(storyDimensions)
|
||||
@ -2730,7 +2730,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
||||
if let self, let image {
|
||||
Queue.mainQueue().async {
|
||||
self.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, true)), scale: 2.5)
|
||||
self.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, .rectangle)), scale: 2.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4110,7 +4110,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
let images = imageItems as! [UIImage]
|
||||
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
|
||||
self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, false)), scale: 2.5)
|
||||
self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image, .sticker)), scale: 2.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -978,7 +978,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
let fadeTransition = Transition.easeInOut(duration: 0.25)
|
||||
if let searchStateContext = self.searchStateContext, case let .search(query) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty {
|
||||
if let searchStateContext = self.searchStateContext, case let .search(query, _) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty {
|
||||
let sideInset: CGFloat = 44.0
|
||||
let emptyAnimationHeight = 148.0
|
||||
let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0
|
||||
@ -1280,10 +1280,14 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
)
|
||||
|
||||
if !self.navigationTextFieldState.text.isEmpty {
|
||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) {
|
||||
var onlyContacts = false
|
||||
if component.initialPrivacy.base == .closeFriends || component.initialPrivacy.base == .contacts {
|
||||
onlyContacts = true
|
||||
}
|
||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
|
||||
} else {
|
||||
self.searchStateDisposable?.dispose()
|
||||
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text))
|
||||
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts))
|
||||
var applyState = false
|
||||
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -1335,7 +1339,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
sideInset: sideInset,
|
||||
title: "Name",
|
||||
peer: nil,
|
||||
subtitle: "sub",
|
||||
subtitle: self.searchStateContext != nil ? "" : "sub",
|
||||
subtitleAccessory: .none,
|
||||
presence: nil,
|
||||
selectionState: .editing(isSelected: false, isTinted: false),
|
||||
@ -1753,7 +1757,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
case stories(editing: Bool)
|
||||
case chats
|
||||
case contacts(EngineStoryPrivacy.Base)
|
||||
case search(String)
|
||||
case search(query: String, onlyContacts: Bool)
|
||||
}
|
||||
|
||||
fileprivate var stateValue: State?
|
||||
@ -1889,7 +1893,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
case let .search(query):
|
||||
case let .search(query, _):
|
||||
self.stateDisposable = (context.engine.contacts.searchLocalPeers(query: query)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
guard let self else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user