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
5ad9671d70
commit
2c05606341
@ -1112,7 +1112,6 @@ public struct StoriesConfiguration {
|
||||
default:
|
||||
posting = .disabled
|
||||
}
|
||||
posting = .enabled
|
||||
return StoriesConfiguration(posting: posting)
|
||||
} else {
|
||||
return .defaultValue
|
||||
|
@ -108,7 +108,7 @@ private final class CameraContext {
|
||||
|
||||
private let session: CameraSession
|
||||
|
||||
private var mainDeviceContext: CameraDeviceContext
|
||||
private var mainDeviceContext: CameraDeviceContext?
|
||||
private var additionalDeviceContext: CameraDeviceContext?
|
||||
|
||||
private let cameraImageContext = CIContext()
|
||||
@ -132,11 +132,11 @@ private final class CameraContext {
|
||||
|
||||
private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||
private var lastAdditionalSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool) {
|
||||
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, front: Bool) {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
var ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
||||
let size = ciImage.extent.size
|
||||
if mirror {
|
||||
if front {
|
||||
var transform = CGAffineTransformMakeScale(1.0, -1.0)
|
||||
transform = CGAffineTransformTranslate(transform, 0.0, -size.height)
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
@ -144,7 +144,7 @@ private final class CameraContext {
|
||||
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size))
|
||||
if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||
if mirror {
|
||||
if front {
|
||||
CameraSimplePreviewView.saveLastFrontImage(uiImage)
|
||||
} else {
|
||||
CameraSimplePreviewView.saveLastBackImage(uiImage)
|
||||
@ -163,40 +163,7 @@ private final class CameraContext {
|
||||
self.positionValue = configuration.position
|
||||
self._positionPromise = ValuePromise<Camera.Position>(configuration.position)
|
||||
|
||||
self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true, additional: false)
|
||||
self.configure {
|
||||
self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata)
|
||||
}
|
||||
|
||||
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.previewNode?.enqueue(sampleBuffer)
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5 {
|
||||
var mirror = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
self.mainDeviceContext.output.processFaceLandmarks = { [weak self] observations in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let previewView = self.previewView {
|
||||
previewView.drawFaceObservations(observations)
|
||||
}
|
||||
}
|
||||
|
||||
self.mainDeviceContext.output.processCodes = { [weak self] codes in
|
||||
self?.detectedCodesPipe.putNext(codes)
|
||||
}
|
||||
self.setDualCameraEnabled(configuration.isDualEnabled, change: false)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@ -217,10 +184,10 @@ private final class CameraContext {
|
||||
|
||||
func stopCapture(invalidate: Bool = false) {
|
||||
if invalidate {
|
||||
self.mainDeviceContext.device.resetZoom()
|
||||
self.mainDeviceContext?.device.resetZoom()
|
||||
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,11 +204,11 @@ private final class CameraContext {
|
||||
focusMode = .autoFocus
|
||||
exposureMode = .autoExpose
|
||||
}
|
||||
self.mainDeviceContext.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true)
|
||||
self.mainDeviceContext?.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true)
|
||||
}
|
||||
|
||||
func setFps(_ fps: Float64) {
|
||||
self.mainDeviceContext.device.fps = fps
|
||||
self.mainDeviceContext?.device.fps = fps
|
||||
}
|
||||
|
||||
private var modeChange: Camera.ModeChange = .none {
|
||||
@ -259,7 +226,10 @@ private final class CameraContext {
|
||||
|
||||
private var positionValue: Camera.Position = .back
|
||||
func togglePosition() {
|
||||
if self.isDualCameraEnabled {
|
||||
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||
return
|
||||
}
|
||||
if self.isDualCameraEnabled == true {
|
||||
let targetPosition: Camera.Position
|
||||
if case .back = self.positionValue {
|
||||
targetPosition = .front
|
||||
@ -269,13 +239,13 @@ private final class CameraContext {
|
||||
self.positionValue = targetPosition
|
||||
self._positionPromise.set(targetPosition)
|
||||
|
||||
self.mainDeviceContext.output.markPositionChange(position: targetPosition)
|
||||
mainDeviceContext.output.markPositionChange(position: targetPosition)
|
||||
} else {
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext?.invalidate()
|
||||
|
||||
let targetPosition: Camera.Position
|
||||
if case .back = self.mainDeviceContext.device.position {
|
||||
if case .back = mainDeviceContext.device.position {
|
||||
targetPosition = .front
|
||||
} else {
|
||||
targetPosition = .back
|
||||
@ -284,7 +254,7 @@ private final class CameraContext {
|
||||
self._positionPromise.set(targetPosition)
|
||||
self.modeChange = .position
|
||||
|
||||
self.mainDeviceContext.configure(position: targetPosition, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
mainDeviceContext.configure(position: targetPosition, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
|
||||
self.queue.after(0.5) {
|
||||
self.modeChange = .none
|
||||
@ -295,13 +265,13 @@ private final class CameraContext {
|
||||
|
||||
public func setPosition(_ position: Camera.Position) {
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext?.invalidate()
|
||||
|
||||
self._positionPromise.set(position)
|
||||
self.positionValue = position
|
||||
self.modeChange = .position
|
||||
|
||||
self.mainDeviceContext.configure(position: position, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
self.mainDeviceContext?.configure(position: position, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
|
||||
self.queue.after(0.5) {
|
||||
self.modeChange = .none
|
||||
@ -309,103 +279,111 @@ private final class CameraContext {
|
||||
}
|
||||
}
|
||||
|
||||
private var isDualCameraEnabled = false
|
||||
public func setDualCameraEnabled(_ enabled: Bool) {
|
||||
private var isDualCameraEnabled: Bool?
|
||||
public func setDualCameraEnabled(_ enabled: Bool, change: Bool = true) {
|
||||
guard enabled != self.isDualCameraEnabled else {
|
||||
return
|
||||
}
|
||||
self.isDualCameraEnabled = enabled
|
||||
|
||||
self.modeChange = .dualCamera
|
||||
if change {
|
||||
self.modeChange = .dualCamera
|
||||
}
|
||||
|
||||
if enabled {
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext?.invalidate()
|
||||
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: false)
|
||||
self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
self.mainDeviceContext?.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
|
||||
self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: true)
|
||||
self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false)
|
||||
}
|
||||
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self, let mainDeviceContext = self.mainDeviceContext else {
|
||||
return
|
||||
}
|
||||
self.previewNode?.enqueue(sampleBuffer)
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5 {
|
||||
var mirror = false
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording {
|
||||
var front = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror)
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
guard let self, let additionalDeviceContext = self.additionalDeviceContext else {
|
||||
return
|
||||
}
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastAdditionalSnapshotTimestamp + 2.5 {
|
||||
var mirror = false
|
||||
if timestamp > self.lastAdditionalSnapshotTimestamp + 2.5, !additionalDeviceContext.output.isRecording {
|
||||
var front = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror)
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||
self.lastAdditionalSnapshotTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext?.invalidate()
|
||||
self.additionalDeviceContext?.invalidate()
|
||||
self.additionalDeviceContext = nil
|
||||
|
||||
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false)
|
||||
self.mainDeviceContext.configure(position: self.positionValue, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
self.mainDeviceContext?.configure(position: self.positionValue, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
}
|
||||
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self, let mainDeviceContext = self.mainDeviceContext else {
|
||||
return
|
||||
}
|
||||
self.previewNode?.enqueue(sampleBuffer)
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5, !self.mainDeviceContext.output.isRecording {
|
||||
var mirror = false
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording {
|
||||
var front = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror)
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
self.mainDeviceContext?.output.processCodes = { [weak self] codes in
|
||||
self?.detectedCodesPipe.putNext(codes)
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, *), let previewView = self.simplePreviewView {
|
||||
if enabled, let secondaryPreviewView = self.secondaryPreviewView {
|
||||
let _ = (combineLatest(previewView.isPreviewing, secondaryPreviewView.isPreviewing)
|
||||
|> map { first, second in
|
||||
return first && second
|
||||
if change {
|
||||
if #available(iOS 13.0, *), let previewView = self.simplePreviewView {
|
||||
if enabled, let secondaryPreviewView = self.secondaryPreviewView {
|
||||
let _ = (combineLatest(previewView.isPreviewing, secondaryPreviewView.isPreviewing)
|
||||
|> map { first, second in
|
||||
return first && second
|
||||
}
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> delay(0.1, queue: self.queue)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] _ in
|
||||
self?.modeChange = .none
|
||||
})
|
||||
} else {
|
||||
let _ = (previewView.isPreviewing
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] _ in
|
||||
self?.modeChange = .none
|
||||
})
|
||||
}
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> delay(0.1, queue: self.queue)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] _ in
|
||||
self?.modeChange = .none
|
||||
})
|
||||
} else {
|
||||
let _ = (previewView.isPreviewing
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] _ in
|
||||
self?.modeChange = .none
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.queue.after(0.4) {
|
||||
self.modeChange = .none
|
||||
self.queue.after(0.4) {
|
||||
self.modeChange = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -417,15 +395,15 @@ private final class CameraContext {
|
||||
}
|
||||
|
||||
var hasTorch: Signal<Bool, NoError> {
|
||||
return self.mainDeviceContext.device.isTorchAvailable
|
||||
return self.mainDeviceContext?.device.isTorchAvailable ?? .never()
|
||||
}
|
||||
|
||||
func setTorchActive(_ active: Bool) {
|
||||
self.mainDeviceContext.device.setTorchActive(active)
|
||||
self.mainDeviceContext?.device.setTorchActive(active)
|
||||
}
|
||||
|
||||
var isFlashActive: Signal<Bool, NoError> {
|
||||
return self.mainDeviceContext.output.isFlashActive
|
||||
return self.mainDeviceContext?.output.isFlashActive ?? .never()
|
||||
}
|
||||
|
||||
private var _flashMode: Camera.FlashMode = .off {
|
||||
@ -443,19 +421,22 @@ private final class CameraContext {
|
||||
}
|
||||
|
||||
func setZoomLevel(_ zoomLevel: CGFloat) {
|
||||
self.mainDeviceContext.device.setZoomLevel(zoomLevel)
|
||||
self.mainDeviceContext?.device.setZoomLevel(zoomLevel)
|
||||
}
|
||||
|
||||
func setZoomDelta(_ zoomDelta: CGFloat) {
|
||||
self.mainDeviceContext.device.setZoomDelta(zoomDelta)
|
||||
self.mainDeviceContext?.device.setZoomDelta(zoomDelta)
|
||||
}
|
||||
|
||||
func takePhoto() -> Signal<PhotoCaptureResult, NoError> {
|
||||
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||
return .complete()
|
||||
}
|
||||
let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait
|
||||
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||
let dualPosition = self.positionValue
|
||||
return combineLatest(
|
||||
self.mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode),
|
||||
mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode),
|
||||
additionalDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode)
|
||||
) |> map { main, additional in
|
||||
if case let .finished(mainImage, _, _) = main, case let .finished(additionalImage, _, _) = additional {
|
||||
@ -469,29 +450,35 @@ private final class CameraContext {
|
||||
}
|
||||
} |> distinctUntilChanged
|
||||
} else {
|
||||
return self.mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode)
|
||||
return mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode)
|
||||
}
|
||||
}
|
||||
|
||||
public func startRecording() -> Signal<Double, NoError> {
|
||||
self.mainDeviceContext.device.setTorchMode(self._flashMode)
|
||||
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||
return .complete()
|
||||
}
|
||||
mainDeviceContext.device.setTorchMode(self._flashMode)
|
||||
|
||||
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||
return combineLatest(
|
||||
self.mainDeviceContext.output.startRecording(isDualCamera: true, position: self.positionValue),
|
||||
mainDeviceContext.output.startRecording(isDualCamera: true, position: self.positionValue),
|
||||
additionalDeviceContext.output.startRecording(isDualCamera: true)
|
||||
) |> map { value, _ in
|
||||
return value
|
||||
}
|
||||
} else {
|
||||
return self.mainDeviceContext.output.startRecording(isDualCamera: false)
|
||||
return mainDeviceContext.output.startRecording(isDualCamera: false)
|
||||
}
|
||||
}
|
||||
|
||||
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||
return .complete()
|
||||
}
|
||||
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||
return combineLatest(
|
||||
self.mainDeviceContext.output.stopRecording(),
|
||||
mainDeviceContext.output.stopRecording(),
|
||||
additionalDeviceContext.output.stopRecording()
|
||||
) |> mapToSignal { main, additional in
|
||||
if case let .finished(mainResult, _, duration, positionChangeTimestamps, _) = main, case let .finished(additionalResult, _, _, _, _) = additional {
|
||||
@ -506,7 +493,7 @@ private final class CameraContext {
|
||||
}
|
||||
} else {
|
||||
let mirror = self.positionValue == .front
|
||||
return self.mainDeviceContext.output.stopRecording()
|
||||
return mainDeviceContext.output.stopRecording()
|
||||
|> map { result -> VideoCaptureResult in
|
||||
if case let .finished(mainResult, _, duration, positionChangeTimestamps, time) = result {
|
||||
var transitionImage = mainResult.1
|
||||
@ -557,14 +544,16 @@ public final class Camera {
|
||||
public struct Configuration {
|
||||
let preset: Preset
|
||||
let position: Position
|
||||
let isDualEnabled: Bool
|
||||
let audio: Bool
|
||||
let photo: Bool
|
||||
let metadata: Bool
|
||||
let preferredFps: Double
|
||||
|
||||
public init(preset: Preset, position: Position, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double) {
|
||||
public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double) {
|
||||
self.preset = preset
|
||||
self.position = position
|
||||
self.isDualEnabled = isDualEnabled
|
||||
self.audio = audio
|
||||
self.photo = photo
|
||||
self.metadata = metadata
|
||||
|
@ -76,24 +76,9 @@ public class CameraSimplePreviewView: UIView {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.videoPreviewLayer.videoGravity = main ? .resizeAspectFill : .resizeAspect
|
||||
|
||||
self.placeholderView.contentMode = main ? .scaleAspectFill : .scaleAspectFit
|
||||
self.addSubview(self.placeholderView)
|
||||
|
||||
if main {
|
||||
if #available(iOS 13.0, *) {
|
||||
self.previewingDisposable = (self.isPreviewing
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
self?.removePlaceholder(delay: 0.15)
|
||||
})
|
||||
} else {
|
||||
Queue.mainQueue().after(0.35) {
|
||||
self.removePlaceholder(delay: 0.15)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addSubview(self.placeholderView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -2586,7 +2586,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
context: context,
|
||||
account: context.account,
|
||||
sharedContext: context.sharedContext,
|
||||
text: .markdown(text: "Posting stories is currently available only to subscribers of [Telegram Premium]()."),
|
||||
text: .markdown(text: "Posting stories is currently available only\nto subscribers of [Telegram Premium]()."),
|
||||
style: .customBlur(UIColor(rgb: 0x2a2a2a), 2.0),
|
||||
icon: .none,
|
||||
location: .point(location, .top),
|
||||
|
@ -219,6 +219,8 @@ private final class StickerSelectionComponent: Component {
|
||||
|
||||
let topPanelHeight: CGFloat = 42.0
|
||||
|
||||
let defaultToEmoji = component.getController()?.defaultToEmoji ?? false
|
||||
|
||||
let context = component.context
|
||||
let stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
|
||||
context: context,
|
||||
@ -247,7 +249,7 @@ private final class StickerSelectionComponent: Component {
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
availableGifSearchEmojies: [],
|
||||
defaultToEmojiTab: false,
|
||||
defaultToEmojiTab: defaultToEmoji,
|
||||
externalTopPanelContainer: self.panelHostView,
|
||||
externalBottomPanelContainer: nil,
|
||||
displayTopPanelBackground: .blur,
|
||||
@ -1629,6 +1631,7 @@ public class StickerPickerScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
private let theme: PresentationTheme
|
||||
private let inputData: Signal<StickerPickerInputData, NoError>
|
||||
fileprivate let defaultToEmoji: Bool
|
||||
|
||||
private var currentLayout: ContainerViewLayout?
|
||||
|
||||
@ -1639,10 +1642,11 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
public var presentGallery: () -> Void = { }
|
||||
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>) {
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false) {
|
||||
self.context = context
|
||||
self.theme = defaultDarkColorPresentationTheme
|
||||
self.inputData = inputData
|
||||
self.defaultToEmoji = defaultToEmoji
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
|
@ -642,7 +642,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
#endif
|
||||
|
||||
if case .notDetermined = cameraAccess, !self.requestedCameraAccess {
|
||||
if !stories, case .notDetermined = cameraAccess, !self.requestedCameraAccess {
|
||||
self.requestedCameraAccess = true
|
||||
self.mediaAssetsContext.requestCameraAccess()
|
||||
}
|
||||
@ -1303,6 +1303,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
if case let .noAccess(cameraAccess) = self.state {
|
||||
var hasCamera = cameraAccess == .authorized
|
||||
if let subject = self.controller?.subject, case .assets(_, .story) = subject {
|
||||
hasCamera = false
|
||||
|
||||
self.controller?.navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
|
||||
var placeholderTransition = transition
|
||||
let placeholderNode: MediaPickerPlaceholderNode
|
||||
if let current = self.placeholderNode {
|
||||
@ -1326,7 +1333,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
self.updateNavigation(transition: .immediate)
|
||||
}
|
||||
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
|
||||
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: hasCamera, transition: placeholderTransition)
|
||||
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds)
|
||||
} else if let placeholderNode = self.placeholderNode, bannedSendMedia == nil {
|
||||
self.placeholderNode = nil
|
||||
|
@ -224,13 +224,22 @@ public extension TelegramEngine {
|
||||
to peerId: EnginePeer.Id,
|
||||
replyTo replyToMessageId: EngineMessage.Id?,
|
||||
storyId: StoryId? = nil,
|
||||
content: EngineOutgoingMessageContent
|
||||
content: EngineOutgoingMessageContent,
|
||||
silentPosting: Bool = false,
|
||||
scheduleTime: Int32? = nil
|
||||
) -> Signal<[MessageId?], NoError> {
|
||||
let message: EnqueueMessage?
|
||||
var message: EnqueueMessage?
|
||||
if case let .contextResult(results, result) = content {
|
||||
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: false, scheduleTime: nil, correlationId: nil)
|
||||
message = self.outgoingMessageWithChatContextResult(to: peerId, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyToMessageId, replyToStoryId: storyId, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime, correlationId: nil)
|
||||
} else {
|
||||
var attributes: [MessageAttribute] = []
|
||||
if silentPosting {
|
||||
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
|
||||
}
|
||||
if let scheduleTime = scheduleTime {
|
||||
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime))
|
||||
}
|
||||
|
||||
var text: String = ""
|
||||
var mediaReference: AnyMediaReference?
|
||||
switch content {
|
||||
@ -257,6 +266,8 @@ public extension TelegramEngine {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
guard let message = message else {
|
||||
return .complete()
|
||||
}
|
||||
|
@ -391,7 +391,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.lastDualCameraTimestamp = currentTimestamp
|
||||
|
||||
controller.node.dismissAllTooltips()
|
||||
let _ = ApplicationSpecificNotice.incrementStoriesDualCameraTip(accountManager: self.context.sharedContext.accountManager).start()
|
||||
let _ = ApplicationSpecificNotice.incrementStoriesDualCameraTip(accountManager: self.context.sharedContext.accountManager, count: 2).start()
|
||||
|
||||
let isEnabled = !controller.cameraState.isDualCameraEnabled
|
||||
camera.setDualCameraEnabled(isEnabled)
|
||||
@ -772,7 +772,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(dualButton
|
||||
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0 - 52.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 1.0))
|
||||
.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))
|
||||
)
|
||||
@ -1133,13 +1133,7 @@ public class CameraScreen: ViewController {
|
||||
} else if dualCamWasEnabled != isDualCameraEnabled {
|
||||
self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .spring(duration: 0.4))
|
||||
|
||||
updateCameraStoredStateInteractively(engine: self.context.engine) { current in
|
||||
if let current {
|
||||
return current.withIsDualCameraEnabled(isDualCameraEnabled)
|
||||
} else {
|
||||
return CameraStoredState(isDualCameraEnabled: isDualCameraEnabled, dualCameraPosition: self.pipPosition)
|
||||
}
|
||||
}
|
||||
UserDefaults.standard.set(isDualCameraEnabled as NSNumber, forKey: "TelegramStoryCameraIsDualEnabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1170,6 +1164,22 @@ public class CameraScreen: ViewController {
|
||||
self.previewBlurView = BlurView()
|
||||
self.previewBlurView.isUserInteractionEnabled = false
|
||||
|
||||
var isDualCameraEnabled = true
|
||||
if let isDualCameraEnabledValue = UserDefaults.standard.object(forKey: "TelegramStoryCameraIsDualEnabled") as? NSNumber {
|
||||
isDualCameraEnabled = isDualCameraEnabledValue.boolValue
|
||||
}
|
||||
|
||||
var dualCameraPosition: PIPPosition = .topRight
|
||||
if let dualCameraPositionValue = UserDefaults.standard.object(forKey: "TelegramStoryCameraDualPosition") as? NSNumber {
|
||||
dualCameraPosition = PIPPosition(rawValue: dualCameraPositionValue.int32Value) ?? .topRight
|
||||
}
|
||||
self.pipPosition = dualCameraPosition
|
||||
|
||||
var cameraFrontPosition = false
|
||||
if let cameraFrontPositionValue = UserDefaults.standard.object(forKey: "TelegramStoryCameraUseFrontPosition") as? NSNumber, cameraFrontPositionValue.boolValue {
|
||||
cameraFrontPosition = true
|
||||
}
|
||||
|
||||
self.mainPreviewContainerView = UIView()
|
||||
self.mainPreviewContainerView.clipsToBounds = true
|
||||
self.mainPreviewView = CameraSimplePreviewView(frame: .zero, main: true)
|
||||
@ -1178,11 +1188,12 @@ public class CameraScreen: ViewController {
|
||||
self.additionalPreviewContainerView.clipsToBounds = true
|
||||
self.additionalPreviewView = CameraSimplePreviewView(frame: .zero, main: false)
|
||||
|
||||
var cameraFrontPosition = false
|
||||
if let useCameraFrontPosition = UserDefaults.standard.object(forKey: "TelegramStoryCameraUseFrontPosition") as? NSNumber, useCameraFrontPosition.boolValue {
|
||||
cameraFrontPosition = true
|
||||
if isDualCameraEnabled {
|
||||
self.mainPreviewView.resetPlaceholder(front: false)
|
||||
self.additionalPreviewView.resetPlaceholder(front: true)
|
||||
} else {
|
||||
self.mainPreviewView.resetPlaceholder(front: cameraFrontPosition)
|
||||
}
|
||||
self.mainPreviewView.resetPlaceholder(front: cameraFrontPosition)
|
||||
|
||||
self.cameraState = CameraState(
|
||||
mode: .photo,
|
||||
@ -1191,7 +1202,7 @@ public class CameraScreen: ViewController {
|
||||
flashModeDidChange: false,
|
||||
recording: .none,
|
||||
duration: 0.0,
|
||||
isDualCameraEnabled: false
|
||||
isDualCameraEnabled: isDualCameraEnabled
|
||||
)
|
||||
|
||||
self.previewFrameLeftDimView = UIView()
|
||||
@ -1265,6 +1276,35 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
if isDualCameraEnabled {
|
||||
let _ = (combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.mainPreviewView.isPreviewing,
|
||||
self.additionalPreviewView.isPreviewing
|
||||
)
|
||||
|> filter { $0 && $1 }
|
||||
|> take(1)).start(next: { [weak self] _, _ in
|
||||
self?.mainPreviewView.removePlaceholder(delay: 0.15)
|
||||
self?.additionalPreviewView.removePlaceholder(delay: 0.15)
|
||||
})
|
||||
} else {
|
||||
let _ = (self.mainPreviewView.isPreviewing
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
self?.mainPreviewView.removePlaceholder(delay: 0.15)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Queue.mainQueue().after(0.35) {
|
||||
self.mainPreviewView.removePlaceholder(delay: 0.15)
|
||||
if isDualCameraEnabled {
|
||||
self.additionalPreviewView.removePlaceholder(delay: 0.15)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
|
||||
}
|
||||
|
||||
@ -1313,6 +1353,7 @@ public class CameraScreen: ViewController {
|
||||
configuration: Camera.Configuration(
|
||||
preset: .hd1920x1080,
|
||||
position: self.cameraState.position,
|
||||
isDualEnabled: self.cameraState.isDualCameraEnabled,
|
||||
audio: true,
|
||||
photo: true,
|
||||
metadata: false,
|
||||
@ -1341,6 +1382,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
})
|
||||
|
||||
var isFirstTime = true
|
||||
self.changingPositionDisposable = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
camera.modeChange,
|
||||
@ -1397,9 +1439,13 @@ public class CameraScreen: ViewController {
|
||||
})
|
||||
}
|
||||
|
||||
if self.cameraState.isDualCameraEnabled {
|
||||
self.mainPreviewView.removePlaceholder()
|
||||
self.additionalPreviewView.removePlaceholder()
|
||||
if isFirstTime {
|
||||
isFirstTime = false
|
||||
} else {
|
||||
if self.cameraState.isDualCameraEnabled {
|
||||
self.mainPreviewView.removePlaceholder()
|
||||
self.additionalPreviewView.removePlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1508,13 +1554,7 @@ public class CameraScreen: ViewController {
|
||||
self.pipPosition = pipPositionForLocation(layout: layout, position: location, velocity: velocity)
|
||||
self.containerLayoutUpdated(layout: layout, transition: .spring(duration: 0.4))
|
||||
|
||||
updateCameraStoredStateInteractively(engine: self.context.engine) { current in
|
||||
if let current {
|
||||
return current.withDualCameraPosition(self.pipPosition)
|
||||
} else {
|
||||
return CameraStoredState(isDualCameraEnabled: true, dualCameraPosition: self.pipPosition)
|
||||
}
|
||||
}
|
||||
UserDefaults.standard.set(self.pipPosition.rawValue as NSNumber, forKey: "TelegramStoryCameraDualPosition")
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1797,13 +1837,12 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
let parentFrame = self.view.convert(self.bounds, to: nil)
|
||||
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize())
|
||||
let location = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
||||
|
||||
let accountManager = self.context.sharedContext.accountManager
|
||||
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Enable Dual Camera Mode"), location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { point, containerFrame in
|
||||
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: "Tap here to disable\nthe selfie camera"), textAlignment: .center, location: .point(location, .right), displayDuration: .custom(5.0), inset: 16.0, shouldDismissOnTouch: { point, containerFrame in
|
||||
if containerFrame.contains(point) {
|
||||
let _ = ApplicationSpecificNotice.incrementStoriesDualCameraTip(accountManager: accountManager).start()
|
||||
let _ = ApplicationSpecificNotice.incrementStoriesDualCameraTip(accountManager: accountManager, count: 2).start()
|
||||
return .dismiss(consume: true)
|
||||
}
|
||||
return .ignore
|
||||
@ -1846,7 +1885,7 @@ public class CameraScreen: ViewController {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if count < 1 {
|
||||
if count < 2 {
|
||||
self.presentDualCameraTooltip()
|
||||
}
|
||||
})
|
||||
@ -2040,7 +2079,7 @@ public class CameraScreen: ViewController {
|
||||
self.appliedDualCamera = isDualCameraEnabled
|
||||
|
||||
let circleSide = floorToScreenPixels(previewSize.width * 160.0 / 430.0)
|
||||
let circleOffset = CGPoint(x: previewSize.width * 224.0 / 1080.0, y: previewSize.width * 520.0 / 1080.0)
|
||||
let circleOffset = CGPoint(x: previewSize.width * 224.0 / 1080.0, y: previewSize.width * 480.0 / 1080.0)
|
||||
|
||||
var origin: CGPoint
|
||||
switch self.pipPosition {
|
||||
|
@ -1,73 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import MediaEditor
|
||||
|
||||
public final class CameraStoredState: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case isDualCameraEnabled
|
||||
case dualCameraPosition
|
||||
}
|
||||
|
||||
public let isDualCameraEnabled: Bool
|
||||
public let dualCameraPosition: CameraScreen.PIPPosition
|
||||
|
||||
public init(
|
||||
isDualCameraEnabled: Bool,
|
||||
dualCameraPosition: CameraScreen.PIPPosition
|
||||
) {
|
||||
self.isDualCameraEnabled = isDualCameraEnabled
|
||||
self.dualCameraPosition = dualCameraPosition
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.isDualCameraEnabled = try container.decode(Bool.self, forKey: .isDualCameraEnabled)
|
||||
self.dualCameraPosition = CameraScreen.PIPPosition(rawValue: try container.decode(Int32.self, forKey: .dualCameraPosition)) ?? .topRight
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.isDualCameraEnabled, forKey: .isDualCameraEnabled)
|
||||
try container.encode(self.dualCameraPosition.rawValue, forKey: .dualCameraPosition)
|
||||
}
|
||||
|
||||
public func withIsDualCameraEnabled(_ isDualCameraEnabled: Bool) -> CameraStoredState {
|
||||
return CameraStoredState(isDualCameraEnabled: isDualCameraEnabled, dualCameraPosition: self.dualCameraPosition)
|
||||
}
|
||||
|
||||
public func withDualCameraPosition(_ dualCameraPosition: CameraScreen.PIPPosition) -> CameraStoredState {
|
||||
return CameraStoredState(isDualCameraEnabled: self.isDualCameraEnabled, dualCameraPosition: dualCameraPosition)
|
||||
}
|
||||
}
|
||||
|
||||
func cameraStoredState(engine: TelegramEngine) -> Signal<CameraStoredState?, NoError> {
|
||||
let key = EngineDataBuffer(length: 4)
|
||||
key.setInt32(0, value: 0)
|
||||
|
||||
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.cameraState, id: key))
|
||||
|> map { entry -> CameraStoredState? in
|
||||
return entry?.get(CameraStoredState.self)
|
||||
}
|
||||
}
|
||||
|
||||
func updateCameraStoredStateInteractively(engine: TelegramEngine, _ f: @escaping (CameraStoredState?) -> CameraStoredState?) {
|
||||
let key = EngineDataBuffer(length: 4)
|
||||
key.setInt32(0, value: 0)
|
||||
|
||||
let _ = (engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.cameraState, id: key))
|
||||
|> map { entry -> CameraStoredState? in
|
||||
return entry?.get(CameraStoredState.self)
|
||||
}
|
||||
|> mapToSignal { state -> Signal<Never, NoError> in
|
||||
if let updatedState = f(state) {
|
||||
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.cameraState, id: key, item: updatedState)
|
||||
} else {
|
||||
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.cameraState, id: key)
|
||||
}
|
||||
}).start()
|
||||
}
|
@ -37,6 +37,8 @@ public final class ChatScheduleTimeController: ViewController {
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public var dismissed: () -> Void = {}
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, mode: ChatScheduleTimeControllerMode, style: ChatScheduleTimeControllerStyle, currentTime: Int32? = nil, minimalTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
@ -105,6 +107,7 @@ public final class ChatScheduleTimeController: ViewController {
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.dismissed()
|
||||
self.controllerNode.animateOut(completion: completion)
|
||||
}
|
||||
|
||||
|
@ -1136,17 +1136,19 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
|
||||
let compressionProperties: [String: Any]
|
||||
let codecType: AVVideoCodecType
|
||||
|
||||
if hasHEVCHardwareEncoder {
|
||||
var bitrate: Int = 3700
|
||||
if image {
|
||||
var bitrate: Int = 3700
|
||||
if image {
|
||||
bitrate = 5000
|
||||
} else {
|
||||
if duration < 10 {
|
||||
bitrate = 5800
|
||||
} else if duration < 20 {
|
||||
bitrate = 5500
|
||||
} else if duration < 30 {
|
||||
bitrate = 5000
|
||||
} else {
|
||||
if duration < 10 {
|
||||
bitrate = 5500
|
||||
} else if duration < 25 {
|
||||
bitrate = 4500
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasHEVCHardwareEncoder {
|
||||
codecType = AVVideoCodecType.hevc
|
||||
compressionProperties = [
|
||||
AVVideoAverageBitRateKey: bitrate * 1000,
|
||||
@ -1155,7 +1157,7 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
|
||||
} else {
|
||||
codecType = AVVideoCodecType.h264
|
||||
compressionProperties = [
|
||||
AVVideoAverageBitRateKey: 3800000,
|
||||
AVVideoAverageBitRateKey: bitrate * 1000,
|
||||
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
||||
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
|
||||
]
|
||||
|
@ -1117,7 +1117,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
self.deactivateInput()
|
||||
},
|
||||
sendMessageOptionsAction: { },
|
||||
sendMessageOptionsAction: nil,
|
||||
sendStickerAction: { _ in },
|
||||
setMediaRecordingActive: nil,
|
||||
lockMediaRecording: nil,
|
||||
@ -2797,6 +2797,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
private var drawingScreen: DrawingScreen?
|
||||
private var stickerScreen: StickerPickerScreen?
|
||||
private var defaultToEmoji = false
|
||||
|
||||
private var previousDrawingData: Data?
|
||||
private var previousDrawingEntities: [DrawingEntity]?
|
||||
@ -2895,7 +2896,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
switch mode {
|
||||
case .sticker:
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get())
|
||||
self.mediaEditor?.stop()
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji)
|
||||
controller.completion = { [weak self] content in
|
||||
if let self {
|
||||
if let content {
|
||||
@ -2905,8 +2907,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.hasAnyChanges = true
|
||||
self.controller?.isSavingAvailable = true
|
||||
self.controller?.requestLayout(transition: .immediate)
|
||||
|
||||
if case let .file(file) = content {
|
||||
if file.isCustomEmoji {
|
||||
self.defaultToEmoji = true
|
||||
} else {
|
||||
self.defaultToEmoji = false
|
||||
}
|
||||
}
|
||||
}
|
||||
self.stickerScreen = nil
|
||||
self.mediaEditor?.play()
|
||||
}
|
||||
}
|
||||
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
|
||||
@ -3140,7 +3151,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
case bottomRight
|
||||
|
||||
func getPosition(_ size: CGSize) -> CGPoint {
|
||||
let offset = CGPoint(x: 224.0, y: 520.0)
|
||||
let offset = CGPoint(x: 224.0, y: 480.0)
|
||||
switch self {
|
||||
case .topLeft:
|
||||
return CGPoint(x: offset.x, y: offset.y)
|
||||
|
@ -258,7 +258,7 @@ final class StoryPreviewComponent: Component {
|
||||
presentController: { _ in },
|
||||
presentInGlobalOverlay: { _ in },
|
||||
sendMessageAction: { },
|
||||
sendMessageOptionsAction: { },
|
||||
sendMessageOptionsAction: nil,
|
||||
sendStickerAction: { _ in },
|
||||
setMediaRecordingActive: { _, _, _ in },
|
||||
lockMediaRecording: nil,
|
||||
|
@ -46,7 +46,7 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
|
||||
public let mode: Mode
|
||||
public let action: (Mode, Action, Bool) -> Void
|
||||
public let longPressAction: () -> Void
|
||||
public let longPressAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let switchMediaInputMode: () -> Void
|
||||
public let updateMediaCancelFraction: (CGFloat) -> Void
|
||||
public let lockMediaRecording: () -> Void
|
||||
@ -62,7 +62,7 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
public init(
|
||||
mode: Mode,
|
||||
action: @escaping (Mode, Action, Bool) -> Void,
|
||||
longPressAction: @escaping () -> Void,
|
||||
longPressAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
switchMediaInputMode: @escaping () -> Void,
|
||||
updateMediaCancelFraction: @escaping (CGFloat) -> Void,
|
||||
lockMediaRecording: @escaping () -> Void,
|
||||
@ -113,9 +113,14 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: HighlightTrackingButton {
|
||||
public final class View: UIView {
|
||||
private var micButton: ChatTextInputMediaRecordingButton?
|
||||
|
||||
public let button: HighlightTrackingButtonNode
|
||||
public let referenceNode: ContextReferenceContentNode
|
||||
public let containerNode: ContextControllerSourceNode
|
||||
private let sendIconView: UIImageView
|
||||
|
||||
private var moreButton: MoreHeaderButton?
|
||||
|
||||
private var component: MessageInputActionButtonComponent?
|
||||
@ -124,13 +129,31 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
override init(frame: CGRect) {
|
||||
self.sendIconView = UIImageView()
|
||||
|
||||
self.button = HighlightTrackingButtonNode()
|
||||
self.referenceNode = ContextReferenceContentNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.isMultipleTouchEnabled = false
|
||||
self.addSubview(self.button.view)
|
||||
self.containerNode.addSubnode(self.referenceNode)
|
||||
self.referenceNode.view.addSubview(self.sendIconView)
|
||||
self.button.addSubnode(self.containerNode)
|
||||
|
||||
self.addSubview(self.sendIconView)
|
||||
self.containerNode.shouldBegin = { [weak self] location in
|
||||
guard let self, let component = self.component, let _ = component.longPressAction else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component, let longPressAction = component.longPressAction else {
|
||||
return
|
||||
}
|
||||
longPressAction(self, gesture)
|
||||
}
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -141,8 +164,10 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
transition.setSublayerTransform(view: self, transform: CATransform3DMakeScale(scale, scale, 1.0))
|
||||
}
|
||||
|
||||
self.addTarget(self, action: #selector(self.touchDown), for: .touchDown)
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.button.addTarget(self, action: #selector(self.touchDown), forControlEvents: .touchDown)
|
||||
self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
// but.addTarget(self, action: #selector(self.touchDown), for: .touchDown)
|
||||
// self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -163,9 +188,10 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
component.action(component.mode, .up, false)
|
||||
}
|
||||
|
||||
override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
return super.continueTracking(touch, with: event)
|
||||
}
|
||||
// public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// let result = super.hitTest(point, with: event)
|
||||
// return result
|
||||
// }
|
||||
|
||||
func update(component: MessageInputActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
@ -174,6 +200,8 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
|
||||
let themeUpdated = previousComponent?.theme !== component.theme
|
||||
|
||||
self.containerNode.isUserInteractionEnabled = component.longPressAction != nil
|
||||
|
||||
if self.micButton == nil {
|
||||
let micButton = ChatTextInputMediaRecordingButton(
|
||||
context: component.context,
|
||||
@ -240,7 +268,7 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if self.moreButton == nil {
|
||||
if case .more = component.mode, self.moreButton == nil {
|
||||
let moreButton = MoreHeaderButton(color: .white)
|
||||
self.moreButton = moreButton
|
||||
self.addSubnode(moreButton)
|
||||
@ -340,6 +368,10 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.button.view, frame: CGRect(origin: .zero, size: availableSize))
|
||||
transition.setFrame(view: self.containerNode.view, frame: CGRect(origin: .zero, size: availableSize))
|
||||
transition.setFrame(view: self.referenceNode.view, frame: CGRect(origin: .zero, size: availableSize))
|
||||
|
||||
transition.setAlpha(view: self.sendIconView, alpha: sendAlpha)
|
||||
transition.setScale(view: self.sendIconView, scale: sendAlpha == 0.0 ? 0.01 : 1.0)
|
||||
|
||||
|
@ -72,7 +72,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let presentController: (ViewController) -> Void
|
||||
public let presentInGlobalOverlay: (ViewController) -> Void
|
||||
public let sendMessageAction: () -> Void
|
||||
public let sendMessageOptionsAction: () -> Void
|
||||
public let sendMessageOptionsAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let sendStickerAction: (TelegramMediaFile) -> Void
|
||||
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
|
||||
public let lockMediaRecording: (() -> Void)?
|
||||
@ -114,7 +114,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
presentInGlobalOverlay: @escaping (ViewController) -> Void,
|
||||
sendMessageAction: @escaping () -> Void,
|
||||
sendMessageOptionsAction: @escaping () -> Void,
|
||||
sendMessageOptionsAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
sendStickerAction: @escaping (TelegramMediaFile) -> Void,
|
||||
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
|
||||
lockMediaRecording: (() -> Void)?,
|
||||
@ -747,7 +747,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
break
|
||||
}
|
||||
},
|
||||
longPressAction: {},
|
||||
longPressAction: nil,
|
||||
switchMediaInputMode: {
|
||||
},
|
||||
updateMediaCancelFraction: { _ in
|
||||
@ -926,12 +926,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
break
|
||||
}
|
||||
},
|
||||
longPressAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.sendMessageOptionsAction()
|
||||
},
|
||||
longPressAction: component.sendMessageOptionsAction,
|
||||
switchMediaInputMode: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -52,6 +52,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
PostboxViewKey.cachedPeerData(peerId: peerId),
|
||||
PostboxViewKey.storiesState(key: .peer(peerId)),
|
||||
PostboxViewKey.storyItems(peerId: peerId),
|
||||
PostboxViewKey.peerPresences(peerIds: Set([peerId]))
|
||||
]
|
||||
if peerId == context.account.peerId {
|
||||
inputKeys.append(PostboxViewKey.storiesState(key: .local))
|
||||
@ -112,6 +113,11 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
return
|
||||
}
|
||||
let additionalPeerData: StoryContentContextState.AdditionalPeerData
|
||||
var peerPresence: PeerPresence?
|
||||
if let presencesView = views.views[PostboxViewKey.peerPresences(peerIds: Set([peerId]))] as? PeerPresencesView {
|
||||
peerPresence = presencesView.presences[peerId]
|
||||
}
|
||||
|
||||
if let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView, let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData {
|
||||
var isMuted = false
|
||||
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
||||
@ -119,9 +125,17 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
} else {
|
||||
isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: nil)
|
||||
}
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(isMuted: isMuted, areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable)
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(
|
||||
isMuted: isMuted,
|
||||
areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable,
|
||||
presence: peerPresence.flatMap { EnginePeer.Presence($0) }
|
||||
)
|
||||
} else {
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(isMuted: true, areVoiceMessagesAvailable: true)
|
||||
additionalPeerData = StoryContentContextState.AdditionalPeerData(
|
||||
isMuted: true,
|
||||
areVoiceMessagesAvailable: true,
|
||||
presence: peerPresence.flatMap { EnginePeer.Presence($0) }
|
||||
)
|
||||
}
|
||||
let state = stateView.value?.get(Stories.PeerState.self)
|
||||
|
||||
@ -923,6 +937,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
self.storyDisposable = (combineLatest(queue: .mainQueue(),
|
||||
context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: storyId.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Presence(id: storyId.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: storyId.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId),
|
||||
TelegramEngine.EngineData.Item.NotificationSettings.Global()
|
||||
@ -960,7 +975,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
return
|
||||
}
|
||||
|
||||
let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
|
||||
let (peer, presence, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
|
||||
let (item, peers, allEntityFiles) = itemAndPeers
|
||||
|
||||
guard let peer else {
|
||||
@ -971,7 +986,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
||||
|
||||
let additionalPeerData = StoryContentContextState.AdditionalPeerData(
|
||||
isMuted: isMuted,
|
||||
areVoiceMessagesAvailable: areVoiceMessagesAvailable
|
||||
areVoiceMessagesAvailable: areVoiceMessagesAvailable,
|
||||
presence: presence
|
||||
)
|
||||
|
||||
if item == nil {
|
||||
@ -1098,6 +1114,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
self.storyDisposable = (combineLatest(queue: .mainQueue(),
|
||||
context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Presence(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId),
|
||||
TelegramEngine.EngineData.Item.NotificationSettings.Global()
|
||||
@ -1111,7 +1128,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
return
|
||||
}
|
||||
|
||||
let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
|
||||
let (peer, presence, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data
|
||||
|
||||
guard let peer else {
|
||||
return
|
||||
@ -1121,7 +1138,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
|
||||
|
||||
let additionalPeerData = StoryContentContextState.AdditionalPeerData(
|
||||
isMuted: isMuted,
|
||||
areVoiceMessagesAvailable: areVoiceMessagesAvailable
|
||||
areVoiceMessagesAvailable: areVoiceMessagesAvailable,
|
||||
presence: presence
|
||||
)
|
||||
|
||||
self.listState = state
|
||||
|
@ -113,19 +113,31 @@ public final class StoryContentItem: Equatable {
|
||||
|
||||
public final class StoryContentContextState {
|
||||
public final class AdditionalPeerData: Equatable {
|
||||
public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool {
|
||||
return lhs.isMuted == rhs.isMuted && lhs.areVoiceMessagesAvailable == rhs.areVoiceMessagesAvailable
|
||||
}
|
||||
|
||||
public let isMuted: Bool
|
||||
public let areVoiceMessagesAvailable: Bool
|
||||
public let presence: EnginePeer.Presence?
|
||||
|
||||
public init(
|
||||
isMuted: Bool,
|
||||
areVoiceMessagesAvailable: Bool
|
||||
areVoiceMessagesAvailable: Bool,
|
||||
presence: EnginePeer.Presence?
|
||||
) {
|
||||
self.isMuted = isMuted
|
||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||
self.presence = presence
|
||||
}
|
||||
|
||||
public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool {
|
||||
if lhs.isMuted != rhs.isMuted {
|
||||
return false
|
||||
}
|
||||
if lhs.areVoiceMessagesAvailable != rhs.areVoiceMessagesAvailable {
|
||||
return false
|
||||
}
|
||||
if lhs.presence != rhs.presence {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1728,11 +1728,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.sendMessageContext.performSendMessageAction(view: self)
|
||||
},
|
||||
sendMessageOptionsAction: { [weak self] in
|
||||
sendMessageOptionsAction: { [weak self] sourceView, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendMessageContext.presentSendMessageOptions(view: self)
|
||||
self.sendMessageContext.presentSendMessageOptions(view: self, sourceView: sourceView, gesture: gesture)
|
||||
},
|
||||
sendStickerAction: { [weak self] sticker in
|
||||
guard let self else {
|
||||
@ -2231,7 +2231,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
mode: .more,
|
||||
action: { _, _, _ in
|
||||
},
|
||||
longPressAction: {},
|
||||
longPressAction: nil,
|
||||
switchMediaInputMode: {
|
||||
},
|
||||
updateMediaCancelFraction: { _ in
|
||||
@ -2838,7 +2838,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
presentationData: presentationData,
|
||||
content: .sticker(context: context, file: animation, loop: false, title: nil, text: "Reaction Sent.", undoText: "View in Chat", customAction: { [weak self] in
|
||||
if let messageId = messageIds.first, let self {
|
||||
self.navigateToPeer(peer: peer, chat: true, messageId: messageId)
|
||||
self.navigateToPeer(peer: peer, chat: true, subject: messageId.flatMap { .message(id: .id($0), highlight: false, timecode: nil) })
|
||||
}
|
||||
}),
|
||||
elevatedLayout: false,
|
||||
@ -3179,7 +3179,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func navigateToPeer(peer: EnginePeer, chat: Bool, messageId: EngineMessage.Id? = nil) {
|
||||
func navigateToPeer(peer: EnginePeer, chat: Bool, subject: ChatControllerSubject? = nil) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
@ -3189,11 +3189,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
guard let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
if messageId != nil || chat {
|
||||
var subject: ChatControllerSubject?
|
||||
if let messageId {
|
||||
subject = .message(id: .id(messageId), highlight: false, timecode: nil)
|
||||
}
|
||||
if subject != nil || chat {
|
||||
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in
|
||||
guard let controller, let navigationController else {
|
||||
return
|
||||
@ -3537,12 +3533,12 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func performMyMoreAction(sourceView: UIView, gesture: ContextGesture?) {
|
||||
func dismissAllTooltips() {
|
||||
guard let component = self.component, let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
component.controller()?.forEachController { c in
|
||||
controller.forEachController { c in
|
||||
if let c = c as? UndoOverlayController {
|
||||
c.dismiss()
|
||||
} else if let c = c as? TooltipScreen {
|
||||
@ -3550,6 +3546,14 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func performMyMoreAction(sourceView: UIView, gesture: ContextGesture?) {
|
||||
guard let component = self.component, let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
var items: [ContextMenuItem] = []
|
||||
@ -3753,7 +3757,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let contextItems = ContextController.Items(content: .list(items), tip: tip, tipSignal: tipSignal)
|
||||
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(contextItems), gesture: gesture)
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: gesture)
|
||||
contextController.dismissed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -3779,12 +3783,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
component.controller()?.forEachController { c in
|
||||
if let c = c as? UndoOverlayController {
|
||||
c.dismiss()
|
||||
}
|
||||
return true
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
var items: [ContextMenuItem] = []
|
||||
@ -3976,7 +3975,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let contextItems = ContextController.Items(content: .list(items), tip: tip, tipSignal: tipSignal)
|
||||
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(contextItems), gesture: gesture)
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .bottom)), items: .single(contextItems), gesture: gesture)
|
||||
contextController.dismissed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -4000,20 +3999,22 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
||||
final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let controller: ViewController
|
||||
private let sourceView: UIView
|
||||
private let position: ContextControllerReferenceViewInfo.ActionsPosition
|
||||
var keepInPlace: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(controller: ViewController, sourceView: UIView) {
|
||||
init(controller: ViewController, sourceView: UIView, position: ContextControllerReferenceViewInfo.ActionsPosition) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
self.position = position
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .bottom)
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.position)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,8 @@ import OpenInExternalAppUI
|
||||
import SafariServices
|
||||
import MediaPasteboardUI
|
||||
import WebPBinding
|
||||
import ContextUI
|
||||
import ChatScheduleTimeController
|
||||
|
||||
final class StoryItemSetContainerSendMessage {
|
||||
enum InputMode {
|
||||
@ -331,7 +333,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentMessageSentTooltip(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messageId: EngineMessage.Id?) {
|
||||
private func presentMessageSentTooltip(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messageId: EngineMessage.Id?, isScheduled: Bool = false) {
|
||||
guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else {
|
||||
return
|
||||
}
|
||||
@ -341,14 +343,17 @@ final class StoryItemSetContainerSendMessage {
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text = isScheduled ? "Message Scheduled" : "Message Sent"
|
||||
|
||||
let tooltipScreen = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .actionSucceeded(title: "", text: "Message Sent", cancel: messageId != nil ? "View in Chat" : "", destructive: false),
|
||||
content: .actionSucceeded(title: "", text: text, 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)
|
||||
view?.navigateToPeer(peer: peer, chat: true, subject: isScheduled ? .scheduledMessages : .message(id: .id(messageId), highlight: false, timecode: nil))
|
||||
}
|
||||
self?.tooltipScreen = nil
|
||||
view?.updateIsProgressPaused()
|
||||
@ -360,12 +365,110 @@ final class StoryItemSetContainerSendMessage {
|
||||
view.updateIsProgressPaused()
|
||||
}
|
||||
|
||||
func presentSendMessageOptions(view: StoryItemSetContainerComponent.View) {
|
||||
func presentSendMessageOptions(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) {
|
||||
guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else {
|
||||
return
|
||||
}
|
||||
|
||||
view.dismissAllTooltips()
|
||||
|
||||
var sendWhenOnlineAvailable = false
|
||||
if let presence = component.slice.additionalPeerData.presence, case .present = presence.status {
|
||||
sendWhenOnlineAvailable = true
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self, weak view] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
self.performSendMessageAction(view: view, silentPosting: true)
|
||||
})))
|
||||
|
||||
if sendWhenOnlineAvailable {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self, weak view] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
self.performSendMessageAction(view: view, scheduleTime: scheduleWhenOnlineTimestamp)
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self, weak view] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
self.presentScheduleTimePicker(view: view)
|
||||
})))
|
||||
|
||||
|
||||
let contextItems = ContextController.Items(content: .list(items))
|
||||
|
||||
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), items: .single(contextItems), gesture: gesture)
|
||||
contextController.dismissed = { [weak view] in
|
||||
guard let view else {
|
||||
return
|
||||
}
|
||||
view.contextController = nil
|
||||
view.updateIsProgressPaused()
|
||||
}
|
||||
view.contextController = contextController
|
||||
view.updateIsProgressPaused()
|
||||
controller.present(contextController, in: .window(.root))
|
||||
}
|
||||
|
||||
func presentScheduleTimePicker(
|
||||
view: StoryItemSetContainerComponent.View
|
||||
) {
|
||||
guard let component = view.component else {
|
||||
return
|
||||
}
|
||||
let focusedItem = component.slice.item
|
||||
guard let peerId = focusedItem.peerId else {
|
||||
return
|
||||
}
|
||||
let controller = component.controller() as? StoryContainerScreen
|
||||
|
||||
var sendWhenOnlineAvailable = false
|
||||
if let presence = component.slice.additionalPeerData.presence, case .present = presence.status {
|
||||
sendWhenOnlineAvailable = true
|
||||
}
|
||||
|
||||
let timeController = ChatScheduleTimeController(context: component.context, updatedPresentationData: nil, peerId: peerId, mode: .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable), style: .media, currentTime: nil, minimalTime: nil, dismissByTapOutside: true, completion: { [weak self, weak view] time in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
self.performSendMessageAction(view: view, scheduleTime: time)
|
||||
})
|
||||
timeController.dismissed = { [weak self, weak view] in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
self.actionSheet = nil
|
||||
view.updateIsProgressPaused()
|
||||
}
|
||||
view.endEditing(true)
|
||||
controller?.present(timeController, in: .window(.root))
|
||||
|
||||
self.actionSheet = timeController
|
||||
view.updateIsProgressPaused()
|
||||
}
|
||||
|
||||
func performSendMessageAction(
|
||||
view: StoryItemSetContainerComponent.View
|
||||
view: StoryItemSetContainerComponent.View,
|
||||
silentPosting: Bool = false,
|
||||
scheduleTime: Int32? = nil
|
||||
) {
|
||||
guard let component = view.component else {
|
||||
return
|
||||
@ -406,11 +509,13 @@ final class StoryItemSetContainerSendMessage {
|
||||
to: peerId,
|
||||
replyTo: nil,
|
||||
storyId: focusedStoryId,
|
||||
content: .text(text.string, entities)
|
||||
content: .text(text.string, entities),
|
||||
silentPosting: silentPosting,
|
||||
scheduleTime: scheduleTime
|
||||
) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
|
||||
Queue.mainQueue().after(0.3) {
|
||||
if let self, let view {
|
||||
self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
|
||||
self.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }, isScheduled: scheduleTime != nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -2181,7 +2286,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
|
||||
Queue.mainQueue().after(0.3) {
|
||||
if let view {
|
||||
self?.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 })
|
||||
self?.presentMessageSentTooltip(view: view, peer: peer, messageId: messageIds.first.flatMap { $0 }, isScheduled: scheduleTime != nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -78,7 +78,6 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||
case translationState = 10
|
||||
case storySource = 11
|
||||
case mediaEditorState = 12
|
||||
case cameraState = 13
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificItemCacheCollectionId {
|
||||
@ -93,7 +92,6 @@ public struct ApplicationSpecificItemCacheCollectionId {
|
||||
public static let translationState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.translationState.rawValue)
|
||||
public static let storySource = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.storySource.rawValue)
|
||||
public static let mediaEditorState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaEditorState.rawValue)
|
||||
public static let cameraState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cameraState.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 {
|
||||
|
@ -542,7 +542,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
animationSpacing = 8.0
|
||||
}
|
||||
|
||||
let containerWidth = max(100.0, min(layout.size.width, 614.0) - (sideInset + layout.safeInsets.left) * 2.0)
|
||||
let containerWidth = max(100.0, min(layout.size.width, 614.0) - sideInset * 2.0)
|
||||
|
||||
var actionSize: CGSize = .zero
|
||||
|
||||
@ -652,11 +652,12 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
self.arrowNode.frame = arrowBounds
|
||||
self.arrowGradientNode?.frame = CGRect(origin: CGPoint(x: -arrowFrame.minX + backgroundFrame.minX, y: 0.0), size: backgroundFrame.size)
|
||||
case .right:
|
||||
arrowFrame = CGRect(origin: CGPoint(x: backgroundFrame.width + arrowSize.height, y: rect.midY), size: CGSize(width: arrowSize.height, height: arrowSize.width))
|
||||
let arrowCenterY = floorToScreenPixels(rect.midY - arrowSize.height / 2.0)
|
||||
arrowFrame = CGRect(origin: CGPoint(x: backgroundFrame.width + arrowSize.height, y: self.view.convert(CGPoint(x: 0.0, y: arrowCenterY), to: self.arrowContainer.supernode?.view).y), size: CGSize(width: arrowSize.height, height: arrowSize.width))
|
||||
|
||||
ContainedViewLayoutTransition.immediate.updateTransformRotation(node: self.arrowContainer, angle: -CGFloat.pi / 2.0)
|
||||
|
||||
transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: 8.0 - UIScreenPixel, dy: 16.0 + -backgroundFrame.minY - floorToScreenPixels((backgroundFrame.height + 20.0 - arrowSize.width) / 2.0)))
|
||||
transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: 8.0 - UIScreenPixel, dy: 0.0))
|
||||
|
||||
let arrowBounds = CGRect(origin: .zero, size: arrowSize)
|
||||
self.arrowNode.frame = arrowBounds
|
||||
|
Loading…
x
Reference in New Issue
Block a user