mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
0833da0e0a
@ -10,6 +10,7 @@ private final class CameraContext {
|
||||
private let device: CameraDevice
|
||||
private let input = CameraInput()
|
||||
private let output = CameraOutput()
|
||||
private let cameraImageContext = CIContext()
|
||||
|
||||
private let initialConfiguration: Camera.Configuration
|
||||
private var invalidated = false
|
||||
@ -40,20 +41,24 @@ private final class CameraContext {
|
||||
}
|
||||
}
|
||||
|
||||
private let previewSnapshotContext = CIContext()
|
||||
private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) {
|
||||
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool) {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
var ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
||||
let size = ciImage.extent.size
|
||||
if mirror {
|
||||
var transform = CGAffineTransformMakeScale(-1.0, 1.0)
|
||||
transform = CGAffineTransformTranslate(transform, size.width, 0.0)
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
}
|
||||
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size))
|
||||
if let cgImage = self.previewSnapshotContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||
CameraSimplePreviewView.saveLastStateImage(uiImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var videoOrientation: AVCaptureVideoOrientation?
|
||||
init(queue: Queue, session: AVCaptureSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?) {
|
||||
self.queue = queue
|
||||
@ -78,27 +83,13 @@ private final class CameraContext {
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5 {
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer)
|
||||
var mirror = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
// if let previewView = self.previewView, !self.changingPosition {
|
||||
// let videoOrientation = connection.videoOrientation
|
||||
// if #available(iOS 13.0, *) {
|
||||
// previewView.mirroring = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
// }
|
||||
// if let rotation = CameraPreviewView.Rotation(with: .portrait, videoOrientation: videoOrientation, cameraPosition: self.device.position) {
|
||||
// previewView.rotation = rotation
|
||||
// }
|
||||
// if #available(iOS 13.0, *), connection.inputPorts.first?.sourceDevicePosition == .front {
|
||||
// let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
// let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
// previewView.captureDeviceResolution = CGSize(width: width, height: height)
|
||||
// }
|
||||
// previewView.pixelBuffer = pixelBuffer
|
||||
// Queue.mainQueue().async {
|
||||
// self.videoOrientation = videoOrientation
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
self.output.processFaceLandmarks = { [weak self] observations in
|
||||
@ -240,7 +231,7 @@ private final class CameraContext {
|
||||
return self.output.startRecording()
|
||||
}
|
||||
|
||||
public func stopRecording() -> Signal<String?, NoError> {
|
||||
public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
|
||||
return self.output.stopRecording()
|
||||
}
|
||||
|
||||
@ -375,7 +366,7 @@ public final class Camera {
|
||||
}
|
||||
}
|
||||
|
||||
public func stopRecording() -> Signal<String?, NoError> {
|
||||
public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.queue.async {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import CoreImage
|
||||
import Vision
|
||||
import VideoToolbox
|
||||
|
||||
@ -160,7 +163,7 @@ final class CameraOutput: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private var recordingCompletionPipe = ValuePipe<String?>()
|
||||
private var recordingCompletionPipe = ValuePipe<(String, UIImage?)?>()
|
||||
func startRecording() -> Signal<Double, NoError> {
|
||||
guard self.videoRecorder == nil else {
|
||||
return .complete()
|
||||
@ -184,14 +187,13 @@ final class CameraOutput: NSObject {
|
||||
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
|
||||
let outputFileURL = URL(fileURLWithPath: outputFilePath)
|
||||
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in
|
||||
if case .success = result {
|
||||
self?.recordingCompletionPipe.putNext(outputFilePath)
|
||||
if case let .success(transitionImage) = result {
|
||||
self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage))
|
||||
} else {
|
||||
self?.recordingCompletionPipe.putNext(nil)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
videoRecorder?.start()
|
||||
self.videoRecorder = videoRecorder
|
||||
|
||||
@ -207,7 +209,7 @@ final class CameraOutput: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
func stopRecording() -> Signal<String?, NoError> {
|
||||
func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
|
||||
self.videoRecorder?.stop()
|
||||
|
||||
return self.recordingCompletionPipe.signal()
|
||||
|
@ -1,5 +1,7 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import CoreImage
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
@ -32,6 +34,10 @@ private final class VideoRecorderImpl {
|
||||
private var videoInput: AVAssetWriterInput?
|
||||
private var audioInput: AVAssetWriterInput?
|
||||
|
||||
private let imageContext: CIContext
|
||||
private var transitionImage: UIImage?
|
||||
private var savedTransitionImage = false
|
||||
|
||||
private var pendingAudioSampleBuffers: [CMSampleBuffer] = []
|
||||
|
||||
private var _duration: CMTime = .zero
|
||||
@ -46,7 +52,7 @@ private final class VideoRecorderImpl {
|
||||
private let configuration: VideoRecorder.Configuration
|
||||
private let videoTransform: CGAffineTransform
|
||||
private let url: URL
|
||||
fileprivate var completion: (Bool) -> Void = { _ in }
|
||||
fileprivate var completion: (Bool, UIImage?) -> Void = { _, _ in }
|
||||
|
||||
private let error = Atomic<Error?>(value: nil)
|
||||
|
||||
@ -58,6 +64,7 @@ private final class VideoRecorderImpl {
|
||||
self.configuration = configuration
|
||||
self.videoTransform = videoTransform
|
||||
self.url = fileUrl
|
||||
self.imageContext = CIContext()
|
||||
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else {
|
||||
@ -76,7 +83,7 @@ private final class VideoRecorderImpl {
|
||||
self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||
if let _ = self.hasError() {
|
||||
return
|
||||
@ -91,7 +98,6 @@ private final class VideoRecorderImpl {
|
||||
guard !self.stopped && self.error.with({ $0 }) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
var failed = false
|
||||
if self.videoInput == nil {
|
||||
let videoSettings = self.configuration.videoSettings
|
||||
@ -139,6 +145,17 @@ private final class VideoRecorderImpl {
|
||||
}
|
||||
|
||||
if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData {
|
||||
if !self.savedTransitionImage, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||
self.savedTransitionImage = true
|
||||
|
||||
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||
if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||
} else {
|
||||
self.savedTransitionImage = false
|
||||
}
|
||||
}
|
||||
|
||||
if videoInput.append(sampleBuffer) {
|
||||
self.lastVideoSampleTime = presentationTime
|
||||
let startTime = self.recordingStartSampleTime
|
||||
@ -274,21 +291,21 @@ private final class VideoRecorderImpl {
|
||||
let completion = self.completion
|
||||
if self.recordingStopSampleTime == .invalid {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
completion(false, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let _ = self.error.with({ $0 }) {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
completion(false, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !self.tryAppendingPendingAudioBuffers() {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
completion(false, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -297,21 +314,21 @@ private final class VideoRecorderImpl {
|
||||
self.assetWriter.finishWriting {
|
||||
if let _ = self.assetWriter.error {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
completion(false, nil)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
completion(true, self.transitionImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let _ = self.assetWriter.error {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
completion(false, nil)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
completion(false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -390,7 +407,7 @@ public final class VideoRecorder {
|
||||
case generic
|
||||
}
|
||||
|
||||
case success
|
||||
case success(UIImage?)
|
||||
case initError(Error)
|
||||
case writeError(Error)
|
||||
case finishError(Error)
|
||||
@ -431,10 +448,10 @@ public final class VideoRecorder {
|
||||
return nil
|
||||
}
|
||||
self.impl = impl
|
||||
impl.completion = { [weak self] success in
|
||||
impl.completion = { [weak self] result, transitionImage in
|
||||
if let self {
|
||||
if success {
|
||||
self.completion(.success)
|
||||
if result {
|
||||
self.completion(.success(transitionImage))
|
||||
} else {
|
||||
self.completion(.finishError(.generic))
|
||||
}
|
||||
|
@ -1848,6 +1848,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
if let size = info.size {
|
||||
fetchRange = (0 ..< Int64(size), .default)
|
||||
}
|
||||
#if DEBUG
|
||||
fetchRange = nil
|
||||
#endif
|
||||
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
||||
}
|
||||
}
|
||||
@ -2516,6 +2519,51 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
|
||||
componentView.storyContextPeerAction = { [weak self] sourceNode, gesture, peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c.dismiss(completion: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
|
||||
return
|
||||
}
|
||||
(self.navigationController as? NavigationController)?.pushViewController(controller)
|
||||
})
|
||||
})
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
})))
|
||||
|
||||
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5335,6 +5383,7 @@ private final class ChatListLocationContext {
|
||||
if stateAndFilterId.state.editing {
|
||||
if case .chatList(.root) = self.location {
|
||||
self.rightButton = nil
|
||||
self.storyButton = nil
|
||||
}
|
||||
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle
|
||||
|
||||
@ -5349,6 +5398,7 @@ private final class ChatListLocationContext {
|
||||
} else if isReorderingTabs {
|
||||
if case .chatList(.root) = self.location {
|
||||
self.rightButton = nil
|
||||
self.storyButton = nil
|
||||
}
|
||||
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: presentationData.strings.Common_Done, isBold: true),
|
||||
@ -5419,6 +5469,16 @@ private final class ChatListLocationContext {
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .icon(imageName: "Chat List/AddStoryIcon"),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self, let parentController = self.parentController else {
|
||||
return
|
||||
}
|
||||
parentController.openStoryCamera()
|
||||
}
|
||||
)))
|
||||
} else {
|
||||
self.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: presentationData.strings.Common_Edit, isBold: false),
|
||||
@ -5473,18 +5533,6 @@ private final class ChatListLocationContext {
|
||||
self.proxyButton = nil
|
||||
}
|
||||
|
||||
if case .chatList(.root) = self.location {
|
||||
self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .icon(imageName: "Chat List/AddStoryIcon"),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self, let parentController = self.parentController else {
|
||||
return
|
||||
}
|
||||
parentController.openStoryCamera()
|
||||
}
|
||||
)))
|
||||
}
|
||||
|
||||
self.chatListTitle = titleContent
|
||||
|
||||
if case .chatList(.root) = self.location, checkProxy {
|
||||
|
@ -914,34 +914,30 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
if itemNode.listNode.isTracking {
|
||||
if case let .known(value) = offset {
|
||||
if !self.storiesUnlocked {
|
||||
if value < -1.0 {
|
||||
if value < -50.0 {
|
||||
self.storiesUnlocked = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
HapticFeedback().impact()
|
||||
|
||||
self.currentItemNode.ignoreStoryInsetAdjustment = true
|
||||
self.currentItemNode.allowInsetFixWhileTracking = true
|
||||
self.onStoriesLockedUpdated?(true)
|
||||
self.currentItemNode.ignoreStoryInsetAdjustment = false
|
||||
self.currentItemNode.allowInsetFixWhileTracking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if self.storiesUnlocked {
|
||||
switch offset {
|
||||
case let .known(value):
|
||||
if value >= 94.0 {
|
||||
if self.storiesUnlocked {
|
||||
self.storiesUnlocked = false
|
||||
self.currentItemNode.stopScrolling()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.onStoriesLockedUpdated?(false)
|
||||
}
|
||||
}
|
||||
self.storiesUnlocked = false
|
||||
self.onStoriesLockedUpdated?(false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -957,7 +953,6 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
if value > 94.0 {
|
||||
if self.storiesUnlocked {
|
||||
self.storiesUnlocked = false
|
||||
self.currentItemNode.stopScrolling()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
@ -1720,7 +1715,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.requestLayout(transition: .immediate)
|
||||
//self.controller?.requestLayout(transition: .immediate)
|
||||
self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in
|
||||
|
@ -1214,6 +1214,8 @@ public final class ChatListNode: ListView {
|
||||
|
||||
super.init()
|
||||
|
||||
self.useMainQueueTransactions = true
|
||||
|
||||
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
|
||||
self.verticalScrollIndicatorFollowsOverscroll = true
|
||||
|
||||
@ -3128,6 +3130,7 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
|
||||
var options = transition.options
|
||||
options.insert(.Synchronous)
|
||||
if self.view.window != nil {
|
||||
if !options.contains(.AnimateInsertion) {
|
||||
options.insert(.PreferSynchronousDrawing)
|
||||
|
@ -212,6 +212,62 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func setFrameWithAdditivePosition(view: UIView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||
assert(view.layer.anchorPoint == CGPoint())
|
||||
|
||||
if view.frame == frame {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
|
||||
var completedBounds: Bool?
|
||||
var completedPosition: Bool?
|
||||
let processCompletion: () -> Void = {
|
||||
guard let completedBounds, let completedPosition else {
|
||||
return
|
||||
}
|
||||
completion?(completedBounds && completedPosition)
|
||||
}
|
||||
|
||||
self.setBounds(view: view, bounds: CGRect(origin: view.bounds.origin, size: frame.size), completion: { value in
|
||||
completedBounds = value
|
||||
processCompletion()
|
||||
})
|
||||
self.animatePosition(view: view, from: CGPoint(x: -frame.minX + view.layer.position.x, y: -frame.minY + view.layer.position.y), to: CGPoint(), additive: true, completion: { value in
|
||||
completedPosition = value
|
||||
processCompletion()
|
||||
})
|
||||
view.layer.position = frame.origin
|
||||
}
|
||||
|
||||
public func setFrameWithAdditivePosition(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||
assert(layer.anchorPoint == CGPoint())
|
||||
|
||||
if layer.frame == frame {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
|
||||
var completedBounds: Bool?
|
||||
var completedPosition: Bool?
|
||||
let processCompletion: () -> Void = {
|
||||
guard let completedBounds, let completedPosition else {
|
||||
return
|
||||
}
|
||||
completion?(completedBounds && completedPosition)
|
||||
}
|
||||
|
||||
self.setBounds(layer: layer, bounds: CGRect(origin: layer.bounds.origin, size: frame.size), completion: { value in
|
||||
completedBounds = value
|
||||
processCompletion()
|
||||
})
|
||||
self.animatePosition(layer: layer, from: CGPoint(x: -frame.minX + layer.position.x, y: -frame.minY + layer.position.y), to: CGPoint(), additive: true, completion: { value in
|
||||
completedPosition = value
|
||||
processCompletion()
|
||||
})
|
||||
layer.position = frame.origin
|
||||
}
|
||||
|
||||
public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||
if view.bounds == bounds {
|
||||
completion?(true)
|
||||
|
@ -15,6 +15,7 @@ swift_library(
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
"//submodules/GZip:GZip",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,6 +4,7 @@ import Lottie
|
||||
import AppBundle
|
||||
import HierarchyTrackingLayer
|
||||
import Display
|
||||
import GZip
|
||||
|
||||
public final class LottieAnimationComponent: Component {
|
||||
public struct AnimationItem: Equatable {
|
||||
@ -176,7 +177,14 @@ public final class LottieAnimationComponent: Component {
|
||||
self.didPlayToCompletion = false
|
||||
self.currentCompletion = nil
|
||||
|
||||
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let animation = Animation.filepath(url.path) {
|
||||
var animation: Animation?
|
||||
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let maybeAnimation = Animation.filepath(url.path) {
|
||||
animation = maybeAnimation
|
||||
} else if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
|
||||
animation = try? Animation.from(data: unpackedData, strategy: .codable)
|
||||
}
|
||||
|
||||
if let animation {
|
||||
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
|
||||
switch component.animation.mode {
|
||||
case .still, .animateTransitionFromPrevious:
|
||||
|
@ -206,6 +206,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
public final var dynamicBounceEnabled = true
|
||||
public final var rotated = false
|
||||
public final var experimentalSnapScrollToItem = false
|
||||
public final var useMainQueueTransactions = false
|
||||
|
||||
public final var scrollEnabled: Bool = true {
|
||||
didSet {
|
||||
@ -250,6 +251,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
}
|
||||
public final var snapToBottomInsetUntilFirstInteraction: Bool = false
|
||||
public final var allowInsetFixWhileTracking: Bool = false
|
||||
|
||||
public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
|
||||
@ -595,7 +597,11 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
action()
|
||||
}
|
||||
}*/
|
||||
DispatchQueue.main.async(execute: action)
|
||||
if self.useMainQueueTransactions && Thread.isMainThread {
|
||||
action()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: action)
|
||||
}
|
||||
}
|
||||
|
||||
private func beginReordering(itemNode: ListViewItemNode) {
|
||||
@ -980,7 +986,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.trackingOffset += -deltaY
|
||||
}
|
||||
|
||||
self.enqueueUpdateVisibleItems(synchronous: false)
|
||||
if self.useMainQueueTransactions {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.enqueueUpdateVisibleItems(synchronous: false)
|
||||
}
|
||||
} else {
|
||||
self.enqueueUpdateVisibleItems(synchronous: false)
|
||||
}
|
||||
|
||||
var useScrollDynamics = false
|
||||
|
||||
@ -1630,19 +1642,29 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
|
||||
self.ignoreScrollingEvents = true
|
||||
if topItemFound && bottomItemFound {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
|
||||
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
|
||||
}
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} else if topItemFound {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
}
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} else if bottomItemFound {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
}
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} else if self.itemNodes.isEmpty {
|
||||
self.scroller.contentSize = self.visibleSize
|
||||
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
|
||||
@ -1650,10 +1672,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.lastContentOffset = .zero
|
||||
}
|
||||
} else {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
|
||||
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
|
||||
}
|
||||
if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 {
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
if self.scroller.contentOffset != self.lastContentOffset {
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
}
|
||||
} else {
|
||||
self.lastContentOffset = self.scroller.contentOffset
|
||||
}
|
||||
@ -1662,8 +1688,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
|
||||
private func async(_ f: @escaping () -> Void) {
|
||||
DispatchQueue.global(qos: .userInteractive).async(execute: f)
|
||||
//DispatchQueue.main.async(execute: f)
|
||||
if self.useMainQueueTransactions {
|
||||
if Thread.isMainThread {
|
||||
f()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: f)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.global(qos: .userInteractive).async(execute: f)
|
||||
}
|
||||
}
|
||||
|
||||
private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject<ListViewItemNode>?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimationIsAnimated: Bool, updateAnimationIsCrossfade: Bool, completion: @escaping (QueueLocalObject<ListViewItemNode>, ListViewItemNodeLayout, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
@ -2951,7 +2984,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
|
||||
var offsetFix: CGFloat
|
||||
let insetDeltaOffsetFix: CGFloat = 0.0
|
||||
if self.isTracking || isExperimentalSnapToScrollToItem {
|
||||
if (self.isTracking && !self.allowInsetFixWhileTracking) || isExperimentalSnapToScrollToItem {
|
||||
offsetFix = 0.0
|
||||
} else if self.snapToBottomInsetUntilFirstInteraction {
|
||||
offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom
|
||||
|
@ -1450,7 +1450,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
if let titleView = titleView as? NavigationBarTitleView {
|
||||
let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset)
|
||||
|
||||
let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), sideContentWidth: 0.0, transition: titleViewTransition)
|
||||
let _ = titleView.updateLayout(size: titleFrame.size, clearBounds: CGRect(origin: CGPoint(x: leftTitleInset - titleFrame.minX, y: 0.0), size: CGSize(width: titleWidth, height: titleFrame.height)), transition: titleViewTransition)
|
||||
}
|
||||
|
||||
if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar {
|
||||
|
@ -4,5 +4,5 @@ import UIKit
|
||||
public protocol NavigationBarTitleView {
|
||||
func animateLayoutTransition()
|
||||
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect
|
||||
}
|
||||
|
@ -329,7 +329,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
view.containerView = self
|
||||
|
||||
func processSnap(snapped: Bool, snapView: UIView) {
|
||||
let processSnap: (Bool, UIView) -> Void = { [weak self] snapped, snapView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if snapped {
|
||||
self.insertSubview(snapView, belowSubview: view)
|
||||
@ -348,20 +351,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
switch type {
|
||||
case .centerX:
|
||||
processSnap(snapped: snapped, snapView: self.xAxisView)
|
||||
processSnap(snapped, self.xAxisView)
|
||||
case .centerY:
|
||||
processSnap(snapped: snapped, snapView: self.yAxisView)
|
||||
processSnap(snapped, self.yAxisView)
|
||||
case .top:
|
||||
processSnap(snapped: snapped, snapView: self.topEdgeView)
|
||||
processSnap(snapped, self.topEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case .left:
|
||||
processSnap(snapped: snapped, snapView: self.leftEdgeView)
|
||||
processSnap(snapped, self.leftEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case .right:
|
||||
processSnap(snapped: snapped, snapView: self.rightEdgeView)
|
||||
processSnap(snapped, self.rightEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case .bottom:
|
||||
processSnap(snapped: snapped, snapView: self.bottomEdgeView)
|
||||
processSnap(snapped, self.bottomEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case let .rotation(angle):
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
|
@ -41,7 +41,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
|
||||
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
let leftInset: CGFloat = 0.0
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
@ -56,7 +56,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
|
||||
self.dateNode.frame = CGRect(origin: CGPoint(x: floor((size.width - dateSize.width) / 2.0), y: floor((size.height - dateSize.height - authorNameSize.height - labelsSpacing) / 2.0) + authorNameSize.height + labelsSpacing), size: dateSize)
|
||||
}
|
||||
|
||||
return 0.0
|
||||
return CGRect()
|
||||
}
|
||||
|
||||
func animateLayoutTransition() {
|
||||
|
@ -636,7 +636,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.medium(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)
|
||||
if let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: 0.0, transition: .immediate)
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -644,11 +644,11 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
||||
super.layoutSubviews()
|
||||
|
||||
if let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: 0.0, transition: .immediate)
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.validLayout = (size, clearBounds)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(size)
|
||||
@ -661,7 +661,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
|
||||
self.titleNode.frame = titleFrame
|
||||
self.subtitleNode.frame = subtitleFrame
|
||||
|
||||
return titleSize.width
|
||||
return titleFrame
|
||||
}
|
||||
|
||||
func animateLayoutTransition() {
|
||||
|
@ -413,7 +413,10 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
||||
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
|
||||
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
|
||||
|
||||
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
|
||||
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
|
||||
if !isSeekable {
|
||||
duration = CMTimeMake(value: Int64.min, timescale: duration.timescale)
|
||||
}
|
||||
|
||||
let metrics = avFormatContext.metricsForStream(at: streamIndex)
|
||||
|
||||
@ -465,7 +468,10 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
||||
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
|
||||
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
|
||||
|
||||
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
|
||||
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
|
||||
if !isSeekable {
|
||||
duration = CMTimeMake(value: Int64.min, timescale: duration.timescale)
|
||||
}
|
||||
|
||||
audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0)
|
||||
break
|
||||
|
@ -389,7 +389,8 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
self.fileMap.serialize(manager: self.manager, to: self.metaPath)
|
||||
}
|
||||
case let .progressUpdated(progress):
|
||||
let _ = progress
|
||||
self.fileMap.progressUpdated(progress)
|
||||
self.updateStatusRequests()
|
||||
case let .replaceHeader(data, range):
|
||||
self.processWrite(resourceOffset: 0, data: data, dataRange: range)
|
||||
case let .moveLocalFile(path):
|
||||
@ -576,7 +577,11 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
updatedStatus = .Remote(progress: progress)
|
||||
}
|
||||
} else if self.pendingFetch != nil {
|
||||
updatedStatus = .Fetching(isActive: true, progress: 0.0)
|
||||
if let progress = self.fileMap.progress {
|
||||
updatedStatus = .Fetching(isActive: true, progress: progress)
|
||||
} else {
|
||||
updatedStatus = .Fetching(isActive: true, progress: 0.0)
|
||||
}
|
||||
} else {
|
||||
updatedStatus = .Remote(progress: 0.0)
|
||||
}
|
||||
|
@ -53,10 +53,10 @@ enum MessageContentToUpload {
|
||||
}
|
||||
|
||||
func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, message: Message) -> MessageContentToUpload {
|
||||
return messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media)
|
||||
return messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media)
|
||||
}
|
||||
|
||||
func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload {
|
||||
func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload {
|
||||
var contextResult: OutgoingChatContextResultMessageAttribute?
|
||||
var autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?
|
||||
var autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?
|
||||
@ -87,14 +87,14 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po
|
||||
return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)), .text)
|
||||
} else if let contextResult = contextResult {
|
||||
return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .chatContextResult(contextResult), reuploadInfo: nil, cacheReferenceKey: nil)), .text)
|
||||
} else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) {
|
||||
} else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) {
|
||||
return .signal(mediaResult, .media)
|
||||
} else {
|
||||
return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text)
|
||||
}
|
||||
}
|
||||
|
||||
func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute]) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? {
|
||||
func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute]) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? {
|
||||
if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat, let resource = largest.resource as? SecretFileMediaResource {
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .secretMedia(.inputEncryptedFile(id: resource.fileId, accessHash: resource.accessHash), resource.decryptedSize, resource.key), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
@ -114,7 +114,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
||||
}
|
||||
}
|
||||
}
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file)
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file)
|
||||
} else {
|
||||
if forceReupload {
|
||||
let mediaReference: AnyMediaReference
|
||||
@ -148,7 +148,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
}
|
||||
} else {
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file)
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file)
|
||||
}
|
||||
} else if let contact = media as? TelegramMediaContact {
|
||||
let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "")
|
||||
@ -644,7 +644,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA
|
||||
return .file
|
||||
}
|
||||
|
||||
private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> {
|
||||
private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> {
|
||||
return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, forceRefresh: forceReupload)
|
||||
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
var referenceKey: CachedSentMediaReferenceKey?
|
||||
@ -694,8 +694,19 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
||||
} else {
|
||||
fileReference = .standalone(media: file)
|
||||
}
|
||||
let upload = messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge)
|
||||
|> mapError { _ -> PendingMessageUploadError in return .generic
|
||||
let upload: Signal<MultipartUploadResult?, PendingMessageUploadError> = .single(nil)
|
||||
|> then(
|
||||
messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge)
|
||||
|> mapError { _ -> PendingMessageUploadError in return .generic }
|
||||
|> map(Optional.init)
|
||||
)
|
||||
let resourceStatus: Signal<MediaResourceStatus?, PendingMessageUploadError>
|
||||
if passFetchProgress {
|
||||
resourceStatus = postbox.mediaBox.resourceStatus(file.resource)
|
||||
|> castError(PendingMessageUploadError.self)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
resourceStatus = .single(nil)
|
||||
}
|
||||
var alreadyTransformed = false
|
||||
for attribute in attributes {
|
||||
@ -773,11 +784,21 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return combineLatest(upload, transformedFileAndThumbnail)
|
||||
|> mapToSignal { content, fileAndThumbnailResult -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
|
||||
return combineLatest(upload, transformedFileAndThumbnail, resourceStatus)
|
||||
|> mapToSignal { content, fileAndThumbnailResult, resourceStatus -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
guard let content = content else {
|
||||
if let resourceStatus = resourceStatus, case let .Fetching(_, progress) = resourceStatus {
|
||||
return .single(.progress(progress * 0.33))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
switch content {
|
||||
case let .progress(progress):
|
||||
var progress = progress
|
||||
if passFetchProgress {
|
||||
progress = 0.33 + progress * 0.67
|
||||
}
|
||||
return .single(.progress(progress))
|
||||
case let .inputFile(inputFile):
|
||||
if case let .done(file, thumbnail) = fileAndThumbnailResult {
|
||||
|
@ -59,7 +59,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
|
||||
case let .update(media):
|
||||
let generateUploadSignal: (Bool) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? = { forceReupload in
|
||||
let augmentedMedia = augmentMediaWithReference(media)
|
||||
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: [])
|
||||
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: [])
|
||||
}
|
||||
if let uploadSignal = generateUploadSignal(forceReupload) {
|
||||
uploadedMedia = .single(.progress(0.027))
|
||||
|
@ -521,6 +521,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: false,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
@ -553,6 +554,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
revalidationContext: account.mediaReferenceRevalidationContext,
|
||||
forceReupload: true,
|
||||
isGrouped: false,
|
||||
passFetchProgress: true,
|
||||
peerId: account.peerId,
|
||||
messageId: nil,
|
||||
attributes: [],
|
||||
@ -1047,3 +1049,224 @@ func _internal_getStoryViews(account: Account, ids: [Int32]) -> Signal<[Int32: S
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class EngineStoryViewListContext {
|
||||
public struct LoadMoreToken: Equatable {
|
||||
var id: Int64
|
||||
var timestamp: Int32
|
||||
}
|
||||
|
||||
public final class Item: Equatable {
|
||||
public let peer: EnginePeer
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(
|
||||
peer: EnginePeer,
|
||||
timestamp: Int32
|
||||
) {
|
||||
self.peer = peer
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public struct State: Equatable {
|
||||
public var totalCount: Int
|
||||
public var items: [Item]
|
||||
public var loadMoreToken: LoadMoreToken?
|
||||
|
||||
public init(
|
||||
totalCount: Int,
|
||||
items: [Item],
|
||||
loadMoreToken: LoadMoreToken?
|
||||
) {
|
||||
self.totalCount = totalCount
|
||||
self.items = items
|
||||
self.loadMoreToken = loadMoreToken
|
||||
}
|
||||
}
|
||||
|
||||
private final class Impl {
|
||||
struct NextOffset: Equatable {
|
||||
var id: Int64
|
||||
var timestamp: Int32
|
||||
}
|
||||
|
||||
struct InternalState: Equatable {
|
||||
var totalCount: Int
|
||||
var items: [Item]
|
||||
var canLoadMore: Bool
|
||||
var nextOffset: NextOffset?
|
||||
}
|
||||
|
||||
let queue: Queue
|
||||
|
||||
let account: Account
|
||||
let storyId: Int32
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
var state: InternalState
|
||||
let statePromise = Promise<InternalState>()
|
||||
|
||||
var isLoadingMore: Bool = false
|
||||
|
||||
init(queue: Queue, account: Account, storyId: Int32, views: EngineStoryItem.Views) {
|
||||
self.queue = queue
|
||||
self.account = account
|
||||
self.storyId = storyId
|
||||
|
||||
let initialState = State(totalCount: views.seenCount, items: [], loadMoreToken: LoadMoreToken(id: 0, timestamp: 0))
|
||||
self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil)
|
||||
self.statePromise.set(.single(self.state))
|
||||
|
||||
if initialState.loadMoreToken != nil {
|
||||
self.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func loadMore() {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
self.isLoadingMore = true
|
||||
|
||||
let account = self.account
|
||||
let storyId = self.storyId
|
||||
let currentOffset = self.state.nextOffset
|
||||
let limit = self.state.items.isEmpty ? 50 : 100
|
||||
let signal: Signal<InternalState, NoError> = self.account.postbox.transaction { transaction -> Void in
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<InternalState, NoError> in
|
||||
return account.network.request(Api.functions.stories.getStoryViewsList(id: storyId, offsetDate: currentOffset?.timestamp ?? 0, offsetId: currentOffset?.id ?? 0, limit: Int32(limit)))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.stories.StoryViewsList?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<InternalState, NoError> in
|
||||
return account.postbox.transaction { transaction -> InternalState in
|
||||
switch result {
|
||||
case let .storyViewsList(count, views, users):
|
||||
var peers: [Peer] = []
|
||||
var peerPresences: [PeerId: Api.User] = [:]
|
||||
|
||||
for user in users {
|
||||
let telegramUser = TelegramUser(user: user)
|
||||
peers.append(telegramUser)
|
||||
peerPresences[telegramUser.id] = user
|
||||
}
|
||||
|
||||
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
|
||||
return updated
|
||||
})
|
||||
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||
|
||||
var items: [Item] = []
|
||||
var nextOffset: NextOffset?
|
||||
for view in views {
|
||||
switch view {
|
||||
case let .storyView(userId, date):
|
||||
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) {
|
||||
items.append(Item(peer: EnginePeer(peer), timestamp: date))
|
||||
|
||||
nextOffset = NextOffset(id: userId, timestamp: date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InternalState(totalCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset)
|
||||
case .none:
|
||||
return InternalState(totalCount: 0, items: [], canLoadMore: false, nextOffset: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.disposable.set((signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
struct ItemHash: Hashable {
|
||||
var peerId: EnginePeer.Id
|
||||
}
|
||||
|
||||
var existingItems = Set<ItemHash>()
|
||||
for item in strongSelf.state.items {
|
||||
existingItems.insert(ItemHash(peerId: item.peer.id))
|
||||
}
|
||||
|
||||
for item in state.items {
|
||||
let itemHash = ItemHash(peerId: item.peer.id)
|
||||
if existingItems.contains(itemHash) {
|
||||
continue
|
||||
}
|
||||
existingItems.insert(itemHash)
|
||||
strongSelf.state.items.append(item)
|
||||
}
|
||||
if state.canLoadMore {
|
||||
strongSelf.state.totalCount = max(state.totalCount, strongSelf.state.items.count)
|
||||
} else {
|
||||
strongSelf.state.totalCount = strongSelf.state.items.count
|
||||
}
|
||||
strongSelf.state.canLoadMore = state.canLoadMore
|
||||
strongSelf.state.nextOffset = state.nextOffset
|
||||
|
||||
strongSelf.isLoadingMore = false
|
||||
strongSelf.statePromise.set(.single(strongSelf.state))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public var state: Signal<State, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.statePromise.get().start(next: { state in
|
||||
var loadMoreToken: LoadMoreToken?
|
||||
if let nextOffset = state.nextOffset {
|
||||
loadMoreToken = LoadMoreToken(id: nextOffset.id, timestamp: nextOffset.timestamp)
|
||||
}
|
||||
subscriber.putNext(State(
|
||||
totalCount: state.totalCount,
|
||||
items: state.items,
|
||||
loadMoreToken: loadMoreToken
|
||||
))
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, storyId: Int32, views: EngineStoryItem.Views) {
|
||||
let queue = Queue()
|
||||
self.queue = queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, account: account, storyId: storyId, views: views)
|
||||
})
|
||||
}
|
||||
|
||||
public func loadMore() {
|
||||
self.impl.with { impl in
|
||||
impl.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -878,5 +878,9 @@ public extension TelegramEngine {
|
||||
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> {
|
||||
return _internal_getStoryViewList(account: account, id: id, offsetTimestamp: offsetTimestamp, offsetPeerId: offsetPeerId, limit: limit)
|
||||
}
|
||||
|
||||
public func storyViewList(id: Int32, views: EngineStoryItem.Views) -> EngineStoryViewListContext {
|
||||
return EngineStoryViewListContext(account: self.account, storyId: id, views: views)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -252,9 +252,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
func stopVideoRecording() {
|
||||
self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0)
|
||||
self.resultDisposable.set((self.camera.stopRecording()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] path in
|
||||
if let self, let path {
|
||||
self.completion.invoke(.single(.video(path, PixelDimensions(width: 1080, height: 1920))))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in
|
||||
if let self, let (path, transitionImage) = pathAndTransitionImage {
|
||||
self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920))))
|
||||
}
|
||||
}))
|
||||
self.updated(transition: .spring(duration: 0.4))
|
||||
@ -641,7 +641,7 @@ public class CameraScreen: ViewController {
|
||||
public enum Result {
|
||||
case pendingImage
|
||||
case image(UIImage)
|
||||
case video(String, PixelDimensions)
|
||||
case video(String, UIImage?, PixelDimensions)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft)
|
||||
}
|
||||
|
@ -299,6 +299,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
|
||||
var contentOffsetFraction: CGFloat = 0.0
|
||||
private(set) var centerContentWidth: CGFloat = 0.0
|
||||
private(set) var centerContentOffsetX: CGFloat = 0.0
|
||||
|
||||
init(
|
||||
backPressed: @escaping () -> Void,
|
||||
@ -446,7 +447,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, size: CGSize, transition: Transition) {
|
||||
func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: Transition) {
|
||||
transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size))
|
||||
|
||||
@ -622,6 +623,8 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var centerContentWidth: CGFloat = 0.0
|
||||
var centerContentOffsetX: CGFloat = 0.0
|
||||
if let chatListTitle = content.chatListTitle {
|
||||
var chatListTitleTransition = transition
|
||||
let chatListTitleView: ChatListTitleView
|
||||
@ -639,8 +642,13 @@ public final class ChatListHeaderComponent: Component {
|
||||
chatListTitleView.theme = theme
|
||||
chatListTitleView.strings = strings
|
||||
chatListTitleView.setTitle(chatListTitle, animated: false)
|
||||
let centerContentWidth = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), sideContentWidth: sideContentWidth, transition: transition.containedViewLayoutTransition)
|
||||
self.centerContentWidth = centerContentWidth
|
||||
let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition)
|
||||
centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0)
|
||||
|
||||
//sideWidth + centerWidth + centerOffset = size.width
|
||||
//let centerOffset = -(size.width - (sideContentWidth + centerContentWidth)) * 0.5 + size.width * 0.5
|
||||
let centerOffset = sideContentWidth
|
||||
centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - rightOffset)
|
||||
|
||||
chatListTitleView.openStatusSetup = { [weak self] sourceView in
|
||||
guard let self else {
|
||||
@ -655,7 +663,14 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.toggleIsLocked()
|
||||
}
|
||||
|
||||
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize))
|
||||
let chatListTitleOffset: CGFloat
|
||||
if chatListTitle.activity {
|
||||
chatListTitleOffset = 0.0
|
||||
} else {
|
||||
chatListTitleOffset = (centerOffset + centerContentOffsetX) * sideContentFraction
|
||||
}
|
||||
|
||||
chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: chatListTitleOffset + floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize))
|
||||
} else {
|
||||
if let chatListTitleView = self.chatListTitleView {
|
||||
self.chatListTitleView = nil
|
||||
@ -664,6 +679,8 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
|
||||
self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil
|
||||
self.centerContentWidth = centerContentWidth
|
||||
self.centerContentOffsetX = centerContentOffsetX
|
||||
}
|
||||
}
|
||||
|
||||
@ -678,6 +695,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
private let storyPeerListExternalState = StoryPeerListComponent.ExternalState()
|
||||
private var storyPeerList: ComponentView<Empty>?
|
||||
public var storyPeerAction: ((EnginePeer?) -> Void)?
|
||||
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
|
||||
|
||||
private var effectiveContentView: ContentView? {
|
||||
return self.secondaryContentView ?? self.primaryContentView
|
||||
@ -795,9 +813,6 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.storyPeerList = storyPeerList
|
||||
}
|
||||
|
||||
if let uploadProgress = component.uploadProgress {
|
||||
print("out \(uploadProgress)")
|
||||
}
|
||||
let _ = storyPeerList.update(
|
||||
transition: storyListTransition,
|
||||
component: AnyComponent(StoryPeerListComponent(
|
||||
@ -813,10 +828,16 @@ public final class ChatListHeaderComponent: Component {
|
||||
return
|
||||
}
|
||||
self.storyPeerAction?(peer)
|
||||
},
|
||||
contextPeerAction: { [weak self] sourceNode, gesture, peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.storyContextPeerAction?(sourceNode, gesture, peer)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: self.bounds.width, height: 94.0)
|
||||
containerSize: CGSize(width: availableSize.width, height: 94.0)
|
||||
)
|
||||
}
|
||||
|
||||
@ -861,7 +882,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth * (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
|
||||
primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition)
|
||||
primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
|
||||
@ -900,7 +921,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.secondaryContentView = secondaryContentView
|
||||
self.addSubview(secondaryContentView)
|
||||
}
|
||||
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, size: availableSize, transition: secondaryContentTransition)
|
||||
secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition)
|
||||
secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
|
||||
@ -956,7 +977,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
|
||||
var defaultStoryListX: CGFloat = 0.0
|
||||
if let primaryContentView = self.primaryContentView {
|
||||
defaultStoryListX = floor((self.storyPeerListExternalState.collapsedWidth - primaryContentView.centerContentWidth) * 0.5)
|
||||
defaultStoryListX = floor((self.storyPeerListExternalState.collapsedWidth - primaryContentView.centerContentWidth) * 0.5) + primaryContentView.centerContentOffsetX
|
||||
}
|
||||
|
||||
storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 94.0)))
|
||||
|
@ -143,7 +143,9 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
self.separatorLayer = SimpleLayer()
|
||||
self.separatorLayer.anchorPoint = CGPoint()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -173,10 +175,7 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
|
||||
public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) {
|
||||
var transition = transition
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
transition = .immediate
|
||||
}
|
||||
let transition = transition
|
||||
|
||||
self.rawScrollOffset = offset
|
||||
|
||||
@ -217,9 +216,13 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
let previousHeight = self.backgroundView.bounds.height
|
||||
|
||||
self.backgroundView.update(size: visibleSize, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: visibleSize))
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel)))
|
||||
self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: visibleSize.width, height: 1000.0)))
|
||||
transition.animatePosition(view: self.backgroundView, from: CGPoint(x: 0.0, y: -visibleSize.height + self.backgroundView.layer.position.y), to: CGPoint(), additive: true)
|
||||
self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height)
|
||||
|
||||
transition.setFrameWithAdditivePosition(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel)))
|
||||
|
||||
let searchContentNode: NavigationBarSearchContentNode
|
||||
if let current = self.searchContentNode {
|
||||
@ -253,6 +256,7 @@ public final class ChatListNavigationBar: Component {
|
||||
component.activateSearch(searchContentNode)
|
||||
}
|
||||
)
|
||||
searchContentNode.view.layer.anchorPoint = CGPoint()
|
||||
self.searchContentNode = searchContentNode
|
||||
self.addSubview(searchContentNode.view)
|
||||
}
|
||||
@ -279,11 +283,17 @@ public final class ChatListNavigationBar: Component {
|
||||
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
|
||||
searchContentNode.expansionProgress = 1.0 - searchOffsetFraction
|
||||
|
||||
transition.setFrame(view: searchContentNode.view, frame: searchFrame)
|
||||
transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame)
|
||||
|
||||
searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
var headerTransition = transition
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
headerTransition = .immediate
|
||||
}
|
||||
|
||||
let headerContentSize = self.headerContent.update(
|
||||
transition: transition,
|
||||
transition: headerTransition,
|
||||
component: AnyComponent(ChatListHeaderComponent(
|
||||
sideInset: component.sideInset + 16.0,
|
||||
primaryContent: component.primaryContent,
|
||||
@ -325,9 +335,10 @@ public final class ChatListNavigationBar: Component {
|
||||
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
|
||||
if let headerContentView = self.headerContent.view {
|
||||
if headerContentView.superview == nil {
|
||||
headerContentView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(headerContentView)
|
||||
}
|
||||
transition.setFrame(view: headerContentView, frame: headerContentFrame)
|
||||
transition.setFrameWithAdditivePosition(view: headerContentView, frame: headerContentFrame)
|
||||
}
|
||||
|
||||
if component.tabsNode !== self.tabsNode {
|
||||
@ -349,7 +360,8 @@ public final class ChatListNavigationBar: Component {
|
||||
let tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height - 46.0), size: CGSize(width: visibleSize.width, height: 46.0))
|
||||
|
||||
if let disappearingTabsView = self.disappearingTabsView {
|
||||
transition.setFrame(view: disappearingTabsView, frame: tabsFrame)
|
||||
disappearingTabsView.layer.anchorPoint = CGPoint()
|
||||
transition.setFrameWithAdditivePosition(view: disappearingTabsView, frame: tabsFrame)
|
||||
}
|
||||
|
||||
if let tabsNode = component.tabsNode {
|
||||
@ -357,6 +369,7 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
var tabsNodeTransition = transition
|
||||
if tabsNode.view.superview !== self {
|
||||
tabsNode.view.layer.anchorPoint = CGPoint()
|
||||
tabsNodeTransition = .immediate
|
||||
self.addSubview(tabsNode.view)
|
||||
if !transition.animation.isImmediate {
|
||||
@ -366,7 +379,7 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
}
|
||||
|
||||
tabsNodeTransition.setFrame(view: tabsNode.view, frame: tabsFrame)
|
||||
tabsNodeTransition.setFrameWithAdditivePosition(view: tabsNode.view, frame: tabsFrame)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
||||
|
||||
public var openStatusSetup: ((UIView) -> Void)?
|
||||
|
||||
private var validLayout: (CGSize, CGRect, CGFloat)?
|
||||
private var validLayout: (CGSize, CGRect)?
|
||||
|
||||
public var manualLayout: Bool = false
|
||||
|
||||
@ -316,13 +316,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (size, clearBounds, sideContentWidth)
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.validLayout = (size, clearBounds)
|
||||
|
||||
var indicatorPadding: CGFloat = 0.0
|
||||
let indicatorSize = self.activityIndicator.bounds.size
|
||||
@ -344,9 +344,9 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
||||
|
||||
let combinedHeight = titleSize.height
|
||||
|
||||
let combinedWidth = sideContentWidth + titleSize.width
|
||||
let combinedWidth = titleSize.width
|
||||
|
||||
var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - combinedWidth - indicatorPadding) / 2.0) + sideContentWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - combinedWidth - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
|
||||
|
||||
titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width)
|
||||
|
||||
@ -429,7 +429,15 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
|
||||
}
|
||||
}
|
||||
|
||||
return combinedWidth
|
||||
var resultFrame = titleFrame
|
||||
if !self.lockView.isHidden {
|
||||
resultFrame = resultFrame.union(lockFrame)
|
||||
}
|
||||
if let titleCredibilityIconView = self.titleCredibilityIconView {
|
||||
resultFrame = resultFrame.union(titleCredibilityIconView.frame)
|
||||
}
|
||||
|
||||
return resultFrame
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
|
@ -118,7 +118,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
private let button: HighlightTrackingButtonNode
|
||||
|
||||
var manualLayout: Bool = false
|
||||
private var validLayout: (CGSize, CGRect, CGFloat)?
|
||||
private var validLayout: (CGSize, CGRect)?
|
||||
|
||||
private var titleLeftIcon: ChatTitleIcon = .none
|
||||
private var titleRightIcon: ChatTitleIcon = .none
|
||||
@ -355,8 +355,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.button.isUserInteractionEnabled = isEnabled
|
||||
if !self.updateStatus() {
|
||||
if updated {
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -605,8 +605,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
}
|
||||
|
||||
if self.activityNode.transitionToState(state, animation: .slide) {
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.3, curve: .spring))
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
@ -688,8 +688,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,14 +704,14 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
self.titleContent = titleContent
|
||||
let _ = self.updateStatus()
|
||||
|
||||
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate)
|
||||
if !self.manualLayout, let (size, clearBounds) = self.validLayout {
|
||||
let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (size, clearBounds, sideContentWidth)
|
||||
public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
|
||||
self.validLayout = (size, clearBounds)
|
||||
|
||||
self.button.frame = clearBounds
|
||||
self.contentContainer.frame = clearBounds
|
||||
@ -851,7 +851,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
|
||||
self.pointerInteraction = PointerInteraction(view: self, style: .rectangle(CGSize(width: titleFrame.width + 16.0, height: 40.0)))
|
||||
|
||||
return titleFrame.width
|
||||
return titleFrame
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
@ -1015,7 +1015,7 @@ public final class ChatTitleComponent: Component {
|
||||
}
|
||||
contentView.updateThemeAndStrings(theme: component.theme, strings: component.strings, hasEmbeddedTitleContent: false)
|
||||
|
||||
let _ = contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), sideContentWidth: 0.0, transition: transition.containedViewLayoutTransition)
|
||||
let _ = contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
return availableSize
|
||||
|
@ -23,13 +23,13 @@ public struct MediaEditorPlayerState {
|
||||
public final class MediaEditor {
|
||||
public enum Subject {
|
||||
case image(UIImage, PixelDimensions)
|
||||
case video(String, PixelDimensions)
|
||||
case video(String, UIImage?, PixelDimensions)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft)
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
case let .image(_, dimensions), let .video(_, dimensions):
|
||||
case let .image(_, dimensions), let .video(_, _, dimensions):
|
||||
return dimensions
|
||||
case let .asset(asset):
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
@ -189,7 +189,7 @@ public final class MediaEditor {
|
||||
let duration = asset.duration.seconds
|
||||
let interval = duration / Double(count)
|
||||
for i in 0 ..< count {
|
||||
timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(NSEC_PER_SEC))))
|
||||
timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(1000))))
|
||||
}
|
||||
|
||||
var updatedFrames: [UIImage] = []
|
||||
@ -287,51 +287,59 @@ public final class MediaEditor {
|
||||
colors = mediaEditorGetGradientColors(from: image)
|
||||
}
|
||||
textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1))
|
||||
case let .video(path, _):
|
||||
case let .video(path, transitionImage, _):
|
||||
textureSource = Signal { subscriber in
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let asset = AVURLAsset(url: url)
|
||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||
imageGenerator.appliesPreferredTrackTransform = true
|
||||
imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
if let image {
|
||||
let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image))
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
|
||||
} else {
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black))
|
||||
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
|
||||
if let transitionImage {
|
||||
let colors = mediaEditorGetGradientColors(from: transitionImage)
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
} else {
|
||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
||||
imageGenerator.appliesPreferredTrackTransform = true
|
||||
imageGenerator.maximumSize = CGSize(width: 72, height: 128)
|
||||
imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in
|
||||
if let image {
|
||||
let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image))
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
|
||||
} else {
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black))
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return ActionDisposable {
|
||||
imageGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
return ActionDisposable {
|
||||
imageGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
case let .asset(asset):
|
||||
textureSource = Signal { subscriber in
|
||||
if asset.mediaType == .video {
|
||||
let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: nil, resultHandler: { image, info in
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .fastFormat
|
||||
let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: options, resultHandler: { image, info in
|
||||
if let image {
|
||||
var degraded = false
|
||||
if let info {
|
||||
if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled {
|
||||
return
|
||||
}
|
||||
if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue {
|
||||
degraded = true
|
||||
}
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in
|
||||
if let asset {
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
if !degraded {
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in
|
||||
if let asset {
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
@ -396,7 +404,7 @@ public final class MediaEditor {
|
||||
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
|
||||
if let self {
|
||||
let start = self.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
|
||||
self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000)))
|
||||
self.player?.play()
|
||||
}
|
||||
})
|
||||
@ -449,9 +457,10 @@ public final class MediaEditor {
|
||||
if !play {
|
||||
self.player?.pause()
|
||||
}
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0))
|
||||
if self.targetTimePosition?.0 != targetPosition {
|
||||
self.targetTimePosition = (targetPosition, play)
|
||||
print("targetchange")
|
||||
if !self.updatingTimePosition {
|
||||
self.updateVideoTimePosition()
|
||||
}
|
||||
@ -474,8 +483,10 @@ public final class MediaEditor {
|
||||
return
|
||||
}
|
||||
self.updatingTimePosition = true
|
||||
self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in
|
||||
print("seekupdate")
|
||||
self.player?.currentItem?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in
|
||||
if let self {
|
||||
print("done")
|
||||
if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition {
|
||||
self.updatingTimePosition = false
|
||||
self.targetTimePosition = nil
|
||||
@ -486,24 +497,14 @@ public final class MediaEditor {
|
||||
})
|
||||
}
|
||||
|
||||
public func setVideoTrimStart(_ trimStart: Double) {
|
||||
public func setVideoTrimRange(_ trimRange: Range<Double>) {
|
||||
self.skipRendering = true
|
||||
let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
self.values = self.values.withUpdatedVideoTrimRange(trimRange)
|
||||
self.skipRendering = false
|
||||
|
||||
self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000))
|
||||
}
|
||||
|
||||
public func setVideoTrimEnd(_ trimEnd: Double) {
|
||||
self.skipRendering = true
|
||||
let trimStart = self.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
self.values = self.values.withUpdatedVideoTrimRange(trimRange)
|
||||
|
||||
self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimEnd, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
self.skipRendering = false
|
||||
}
|
||||
|
||||
public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) {
|
||||
self.values = self.values.withUpdatedDrawingAndEntities(drawing: image, entities: entities)
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ final class MediaEditorComposer {
|
||||
}
|
||||
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||
|
||||
self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation, render: true)
|
||||
self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation, timestamp: time, render: true)
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
|
@ -7,7 +7,7 @@ import SwiftSignalKit
|
||||
|
||||
protocol TextureConsumer: AnyObject {
|
||||
func consumeTexture(_ texture: MTLTexture, render: Bool)
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, render: Bool)
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, timestamp: CMTime, render: Bool)
|
||||
}
|
||||
|
||||
final class RenderingContext {
|
||||
@ -247,13 +247,19 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, render: Bool) {
|
||||
var previousPresentationTimestamp: CMTime?
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, timestamp: CMTime, render: Bool) {
|
||||
let _ = self.semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
self.currentPixelBuffer = (pixelBuffer, rotation)
|
||||
if render {
|
||||
self.renderFrame()
|
||||
if self.previousPresentationTimestamp == timestamp {
|
||||
self.semaphore.signal()
|
||||
} else {
|
||||
self.renderFrame()
|
||||
}
|
||||
}
|
||||
self.previousPresentationTimestamp = timestamp
|
||||
}
|
||||
|
||||
func renderTargetDidChange(_ target: RenderTarget?) {
|
||||
|
@ -198,7 +198,7 @@ public final class MediaEditorVideoExport {
|
||||
|
||||
var timeRange: CMTimeRange? {
|
||||
if let videoTrimRange = self.values.videoTrimRange {
|
||||
return CMTimeRange(start: CMTime(seconds: videoTrimRange.lowerBound, preferredTimescale: 1), end: CMTime(seconds: videoTrimRange.upperBound, preferredTimescale: 1))
|
||||
return CMTimeRange(start: CMTime(seconds: videoTrimRange.lowerBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), end: CMTime(seconds: videoTrimRange.upperBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -236,7 +236,7 @@ public final class MediaEditorVideoExport {
|
||||
|
||||
public enum ExportStatus {
|
||||
case unknown
|
||||
case progress(Double)
|
||||
case progress(Float)
|
||||
case completed
|
||||
case failed(ExportError)
|
||||
}
|
||||
@ -259,6 +259,13 @@ public final class MediaEditorVideoExport {
|
||||
|
||||
private var textureRotation: TextureRotation = .rotate0Degrees
|
||||
private let duration = ValuePromise<CMTime>()
|
||||
private var durationValue: CMTime? {
|
||||
didSet {
|
||||
if let durationValue = self.durationValue {
|
||||
self.duration.set(durationValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let pauseDispatchGroup = DispatchGroup()
|
||||
private var cancelled = false
|
||||
@ -279,14 +286,14 @@ public final class MediaEditorVideoExport {
|
||||
private func setup() {
|
||||
if case let .video(asset) = self.subject {
|
||||
if let trimmedVideoDuration = self.configuration.timeRange?.duration {
|
||||
self.duration.set(trimmedVideoDuration)
|
||||
self.durationValue = trimmedVideoDuration
|
||||
} else {
|
||||
asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"]) {
|
||||
self.duration.set(asset.duration)
|
||||
self.durationValue = asset.duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.duration.set(CMTime(seconds: 5, preferredTimescale: 1))
|
||||
self.durationValue = CMTime(seconds: 5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
}
|
||||
|
||||
switch self.subject {
|
||||
@ -325,20 +332,23 @@ public final class MediaEditorVideoExport {
|
||||
let videoTracks = asset.tracks(withMediaType: .video)
|
||||
if (videoTracks.count > 0) {
|
||||
var sourceFrameRate: Float = 0.0
|
||||
let colorProperties: [String: Any] = [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
|
||||
]
|
||||
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
|
||||
AVVideoColorPropertiesKey: [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
|
||||
]
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true,
|
||||
AVVideoColorPropertiesKey: colorProperties
|
||||
]
|
||||
if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing {
|
||||
} else {
|
||||
self.setupComposer()
|
||||
}
|
||||
let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings)
|
||||
videoOutput.alwaysCopiesSampleData = false
|
||||
videoOutput.alwaysCopiesSampleData = true
|
||||
if reader.canAdd(videoOutput) {
|
||||
reader.add(videoOutput)
|
||||
} else {
|
||||
@ -519,8 +529,13 @@ public final class MediaEditorVideoExport {
|
||||
}
|
||||
self.pauseDispatchGroup.wait()
|
||||
if let buffer = output.copyNextSampleBuffer() {
|
||||
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
if let duration = self.durationValue {
|
||||
let startTimestamp = self.reader?.timeRange.start ?? .zero
|
||||
let progress = (timestamp - startTimestamp).seconds / duration.seconds
|
||||
self.statusValue = .progress(Float(progress))
|
||||
}
|
||||
if let composer = self.composer {
|
||||
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in
|
||||
if let pixelBuffer {
|
||||
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
|
||||
@ -595,6 +610,12 @@ public final class MediaEditorVideoExport {
|
||||
self.resume()
|
||||
}
|
||||
self.cancelled = true
|
||||
|
||||
self.queue.async {
|
||||
if let reader = self.reader, reader.status == .reading {
|
||||
reader.cancelReading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let statusPromise = Promise<ExportStatus>(.unknown)
|
||||
@ -607,7 +628,6 @@ public final class MediaEditorVideoExport {
|
||||
return self.statusPromise.get()
|
||||
}
|
||||
|
||||
|
||||
private func startImageVideoExport() {
|
||||
guard self.internalStatus == .idle, let writer = self.writer else {
|
||||
self.statusValue = .failed(.invalid)
|
||||
@ -687,7 +707,7 @@ public final class MediaEditorVideoExport {
|
||||
}
|
||||
}
|
||||
|
||||
public func startExport() {
|
||||
public func start() {
|
||||
switch self.subject {
|
||||
case .video:
|
||||
self.startVideoExport()
|
||||
|
@ -127,7 +127,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
]
|
||||
|
||||
let output = AVPlayerItemVideoOutput(outputSettings: outputSettings)
|
||||
output.suppressesPlayerRendering = true
|
||||
//output.suppressesPlayerRendering = true
|
||||
output.setDelegate(self, queue: self.queue)
|
||||
playerItem.add(output)
|
||||
self.playerItemOutput = output
|
||||
@ -163,7 +163,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
if self.player.rate != 0 {
|
||||
self.forceUpdate = true
|
||||
}
|
||||
self.update(forced: self.forceUpdate)
|
||||
self.update(forced: true) //self.forceUpdate)
|
||||
self.forceUpdate = false
|
||||
}
|
||||
|
||||
@ -186,7 +186,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
|
||||
var presentationTime: CMTime = .zero
|
||||
if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) {
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation, render: true)
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation, timestamp: presentationTime, render: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,8 +612,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
framesUpdateTimestamp: playerState.framesUpdateTimestamp,
|
||||
trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in
|
||||
if let mediaEditor {
|
||||
mediaEditor.setVideoTrimStart(start)
|
||||
mediaEditor.setVideoTrimEnd(end)
|
||||
mediaEditor.setVideoTrimRange(start..<end)
|
||||
if done {
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
} else {
|
||||
@ -1440,7 +1439,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
} else if abs(translation.x) > 10.0 && !self.isDismissing {
|
||||
self.isEnhacing = true
|
||||
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
|
||||
if self.isDismissing {
|
||||
@ -1467,7 +1466,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
} else {
|
||||
self.isEnhacing = false
|
||||
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -1500,6 +1499,29 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupTransitionImage(_ image: UIImage) {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
|
||||
let transitionInView = UIImageView(image: image)
|
||||
var initialScale: CGFloat
|
||||
if image.size.height > image.size.width {
|
||||
initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height)
|
||||
} else {
|
||||
initialScale = self.previewContainerView.bounds.width / image.size.width
|
||||
}
|
||||
transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0)
|
||||
transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale)
|
||||
self.previewContainerView.addSubview(transitionInView)
|
||||
self.transitionInView = transitionInView
|
||||
|
||||
self.mediaEditor?.onFirstDisplay = { [weak self] in
|
||||
if let self, let transitionInView = self.transitionInView {
|
||||
transitionInView.removeFromSuperview()
|
||||
self.transitionInView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let transitionIn = self.controller?.transitionIn {
|
||||
switch transitionIn {
|
||||
@ -1507,28 +1529,12 @@ public final class MediaEditorScreen: ViewController {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .camera)
|
||||
}
|
||||
if let subject = self.subject, case let .video(_, transitionImage, _) = subject, let transitionImage {
|
||||
self.setupTransitionImage(transitionImage)
|
||||
}
|
||||
case let .gallery(transitionIn):
|
||||
if let sourceImage = transitionIn.sourceImage {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
|
||||
let transitionInView = UIImageView(image: sourceImage)
|
||||
var initialScale: CGFloat
|
||||
if sourceImage.size.height > sourceImage.size.width {
|
||||
initialScale = max(self.previewContainerView.bounds.width / sourceImage.size.width, self.previewContainerView.bounds.height / sourceImage.size.height)
|
||||
} else {
|
||||
initialScale = self.previewContainerView.bounds.width / sourceImage.size.width
|
||||
}
|
||||
transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0)
|
||||
transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale)
|
||||
self.previewContainerView.addSubview(transitionInView)
|
||||
self.transitionInView = transitionInView
|
||||
|
||||
self.mediaEditor?.onFirstDisplay = { [weak self] in
|
||||
if let self, let transitionInView = self.transitionInView {
|
||||
transitionInView.removeFromSuperview()
|
||||
self.transitionInView = nil
|
||||
}
|
||||
}
|
||||
self.setupTransitionImage(sourceImage)
|
||||
}
|
||||
if let sourceView = transitionIn.sourceView {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
@ -1714,34 +1720,62 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.controller?.present(tooltipController, in: .current)
|
||||
}
|
||||
|
||||
private var saveTooltip: TooltipScreen?
|
||||
private weak var saveTooltip: SaveProgressScreen?
|
||||
func presentSaveTooltip() {
|
||||
guard let controller = self.controller, let sourceView = self.componentHost.findTaggedView(tag: saveButtonTag) else {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
if let saveTooltip = self.saveTooltip {
|
||||
saveTooltip.dismiss(animated: true)
|
||||
self.saveTooltip = nil
|
||||
if case .completion = saveTooltip.content {
|
||||
saveTooltip.dismiss()
|
||||
self.saveTooltip = nil
|
||||
}
|
||||
}
|
||||
|
||||
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 text: String
|
||||
let isVideo = self.mediaEditor?.resultIsVideo ?? false
|
||||
if isVideo {
|
||||
text = "Video saved to Photos"
|
||||
text = "Video saved to Photos."
|
||||
} else {
|
||||
text = "Image saved to Photos"
|
||||
text = "Image saved to Photos."
|
||||
}
|
||||
|
||||
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, cornerRadius: 10.0, shouldDismissOnTouch: { _ in
|
||||
return .ignore
|
||||
})
|
||||
self.saveTooltip = tooltipController
|
||||
controller.present(tooltipController, in: .current)
|
||||
if let tooltipController = self.saveTooltip {
|
||||
tooltipController.content = .completion(text)
|
||||
} else {
|
||||
let tooltipController = SaveProgressScreen(context: self.context, content: .completion(text))
|
||||
controller.present(tooltipController, in: .current)
|
||||
self.saveTooltip = tooltipController
|
||||
}
|
||||
}
|
||||
|
||||
func updateVideoExportProgress(_ progress: Float) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
if let saveTooltip = self.saveTooltip {
|
||||
if case .completion = saveTooltip.content {
|
||||
saveTooltip.dismiss()
|
||||
self.saveTooltip = nil
|
||||
}
|
||||
}
|
||||
|
||||
let text = "Preparing video..."
|
||||
|
||||
if let tooltipController = self.saveTooltip {
|
||||
tooltipController.content = .progress(text, progress)
|
||||
} else {
|
||||
let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0))
|
||||
tooltipController.cancelled = { [weak self] in
|
||||
if let self, let controller = self.controller {
|
||||
controller.cancelVideoExport()
|
||||
}
|
||||
}
|
||||
controller.present(tooltipController, in: .current)
|
||||
self.saveTooltip = tooltipController
|
||||
}
|
||||
}
|
||||
|
||||
private weak var storyArchiveTooltip: ViewController?
|
||||
@ -2009,13 +2043,13 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
public enum Subject {
|
||||
case image(UIImage, PixelDimensions)
|
||||
case video(String, PixelDimensions)
|
||||
case video(String, UIImage?, PixelDimensions)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft)
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
case let .image(_, dimensions), let .video(_, dimensions):
|
||||
case let .image(_, dimensions), let .video(_, _, dimensions):
|
||||
return dimensions
|
||||
case let .asset(asset):
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
@ -2028,8 +2062,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
switch self {
|
||||
case let .image(image, dimensions):
|
||||
return .image(image, dimensions)
|
||||
case let .video(videoPath, dimensions):
|
||||
return .video(videoPath, dimensions)
|
||||
case let .video(videoPath, transitionImage, dimensions):
|
||||
return .video(videoPath, transitionImage, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
@ -2041,7 +2075,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
switch self {
|
||||
case let .image(image, dimensions):
|
||||
return .image(image, dimensions)
|
||||
case let .video(videoPath, dimensions):
|
||||
case let .video(videoPath, _, dimensions):
|
||||
return .video(videoPath, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
@ -2096,6 +2130,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.exportDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self)
|
||||
|
||||
@ -2273,6 +2311,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
func maybePresentDiscardAlert() {
|
||||
if "".isEmpty {
|
||||
self.requestDismiss(saveDraft: false, animated: true)
|
||||
return
|
||||
}
|
||||
if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false {
|
||||
self.requestDismiss(saveDraft: false, animated: true)
|
||||
return
|
||||
@ -2397,7 +2439,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
videoResult = .imageFile(path: tempImagePath)
|
||||
duration = 5.0
|
||||
case let .video(path, _):
|
||||
case let .video(path, _, _):
|
||||
videoResult = .videoFile(path: path)
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
@ -2430,8 +2472,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
finished()
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -2444,8 +2488,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
if let resultImage {
|
||||
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
finished()
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
if case let .draft(draft) = subject {
|
||||
@ -2458,7 +2504,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
private var videoExport: MediaEditorVideoExport?
|
||||
private var exportDisposable: Disposable?
|
||||
private var exportDisposable = MetaDisposable()
|
||||
|
||||
private var previousSavedValues: MediaEditorValues?
|
||||
func requestSave() {
|
||||
@ -2497,9 +2543,12 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
mediaEditor.stop()
|
||||
self.node.entitiesView.pause()
|
||||
|
||||
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
|
||||
switch subject {
|
||||
case let .video(path, _):
|
||||
case let .video(path, _, _):
|
||||
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
|
||||
exportSubject = .single(.video(asset))
|
||||
case let .image(image, _):
|
||||
@ -2547,28 +2596,46 @@ public final class MediaEditorScreen: ViewController {
|
||||
let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath)
|
||||
self.videoExport = videoExport
|
||||
|
||||
videoExport.startExport()
|
||||
videoExport.start()
|
||||
|
||||
self.exportDisposable = (videoExport.status
|
||||
self.exportDisposable.set((videoExport.status
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let self {
|
||||
if case .completed = status {
|
||||
switch status {
|
||||
case .completed:
|
||||
self.videoExport = nil
|
||||
saveToPhotos(outputPath, true)
|
||||
self.node.presentSaveTooltip()
|
||||
|
||||
self.node.mediaEditor?.play()
|
||||
self.node.entitiesView.play()
|
||||
case let .progress(progress):
|
||||
if self.videoExport != nil {
|
||||
self.node.updateVideoExportProgress(progress)
|
||||
}
|
||||
case .failed:
|
||||
self.videoExport = nil
|
||||
self.node.mediaEditor?.play()
|
||||
self.node.entitiesView.play()
|
||||
case .unknown:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
})
|
||||
} else {
|
||||
if let image = mediaEditor.resultImage {
|
||||
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
||||
try? data.write(to: URL(fileURLWithPath: outputPath))
|
||||
saveToPhotos(outputPath, false)
|
||||
}
|
||||
})
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
|
||||
try? data.write(to: URL(fileURLWithPath: outputPath))
|
||||
Queue.mainQueue().async {
|
||||
saveToPhotos(outputPath, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
self.node.presentSaveTooltip()
|
||||
}
|
||||
}
|
||||
@ -2578,6 +2645,19 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
}
|
||||
|
||||
fileprivate func cancelVideoExport() {
|
||||
if let videoExport = self.videoExport {
|
||||
self.previousSavedValues = nil
|
||||
|
||||
videoExport.cancel()
|
||||
self.videoExport = nil
|
||||
self.exportDisposable.set(nil)
|
||||
|
||||
self.node.mediaEditor?.play()
|
||||
self.node.entitiesView.play()
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
@ -2588,6 +2668,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss()
|
||||
}
|
||||
if let controller = controller as? SaveProgressScreen {
|
||||
controller.dismiss()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
@ -0,0 +1,577 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import LottieAnimationComponent
|
||||
import BundleIconComponent
|
||||
|
||||
private final class ProgressComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let title: String
|
||||
let value: Float
|
||||
let cancel: () -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
value: Float,
|
||||
cancel: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.cancel = cancel
|
||||
}
|
||||
|
||||
static func ==(lhs: ProgressComponent, rhs: ProgressComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let progressLayer = SimpleShapeLayer()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
|
||||
private var component: ProgressComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let lineWidth: CGFloat = 3.0
|
||||
let progressSize = CGSize(width: 42.0, height: 42.0)
|
||||
|
||||
self.progressLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: progressSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), transform: nil)
|
||||
self.progressLayer.lineWidth = lineWidth
|
||||
self.progressLayer.strokeColor = UIColor.white.cgColor
|
||||
self.progressLayer.fillColor = UIColor.clear.cgColor
|
||||
self.progressLayer.lineCap = .round
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.progressLayer.bounds = CGRect(origin: .zero, size: progressSize)
|
||||
|
||||
self.layer.addSublayer(self.progressLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let minWidth: CGFloat = 98.0
|
||||
let inset: CGFloat = 16.0
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Text(text: component.title, font: Font.regular(14.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: 40.0)
|
||||
)
|
||||
|
||||
let width: CGFloat = max(minWidth, titleSize.width + inset * 2.0)
|
||||
let titleFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 16.0),
|
||||
size: titleSize
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
let progressPosition = CGPoint(x: width / 2.0, y: titleFrame.maxY + 34.0)
|
||||
self.progressLayer.position = progressPosition
|
||||
transition.setShapeLayerStrokeEnd(layer: self.progressLayer, strokeEnd: CGFloat(max(0.027, component.value)))
|
||||
|
||||
if self.progressLayer.animation(forKey: "rotation") == nil {
|
||||
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
basicAnimation.duration = 2.0
|
||||
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
||||
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
|
||||
basicAnimation.repeatCount = Float.infinity
|
||||
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
self.progressLayer.add(basicAnimation, forKey: "rotation")
|
||||
}
|
||||
|
||||
let cancelSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Media Gallery/Close",
|
||||
tintColor: UIColor.white
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self, let component = self.component {
|
||||
component.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: 40.0)
|
||||
)
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(progressPosition.x - cancelSize.width / 2.0), y: floorToScreenPixels(progressPosition.y - cancelSize.height / 2.0)), size: cancelSize)
|
||||
if let cancelButtonView = self.cancelButton.view {
|
||||
if cancelButtonView.superview == nil {
|
||||
self.addSubview(cancelButtonView)
|
||||
}
|
||||
cancelButtonView.frame = cancelButtonFrame
|
||||
}
|
||||
|
||||
return CGSize(width: width, height: 104.0)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private final class BannerComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let iconName: String
|
||||
let text: String
|
||||
|
||||
init(
|
||||
iconName: String,
|
||||
text: String
|
||||
) {
|
||||
self.iconName = iconName
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: BannerComponent, rhs: BannerComponent) -> Bool {
|
||||
if lhs.iconName != rhs.iconName {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var component: BannerComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: BannerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let height: CGFloat = 49.0
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(animation: LottieAnimationComponent.AnimationItem(name: component.iconName, mode: .animating(loop: false)), colors: [:], size: CGSize(width: 32.0, height: 32.0))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
let iconFrame = CGRect(
|
||||
origin: CGPoint(x: 9.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)),
|
||||
size: iconSize
|
||||
)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
Text(text: component.text, font: Font.regular(14.0), color: .white)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: height)
|
||||
)
|
||||
|
||||
let textFrame = CGRect(
|
||||
origin: CGPoint(x: iconFrame.maxX + 9.0, y: floorToScreenPixels((height - textSize.height) / 2.0)),
|
||||
size: textSize
|
||||
)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.frame = textFrame
|
||||
}
|
||||
|
||||
return CGSize(width: textFrame.maxX + 12.0, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
final class SaveProgressScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
enum Content: Equatable {
|
||||
enum ContentType: Equatable {
|
||||
case progress
|
||||
case completion
|
||||
}
|
||||
|
||||
case progress(String, Float)
|
||||
case completion(String)
|
||||
|
||||
var type: ContentType {
|
||||
switch self {
|
||||
case .progress:
|
||||
return .progress
|
||||
case .completion:
|
||||
return .completion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let content: Content
|
||||
let cancel: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
content: Content,
|
||||
cancel: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
self.cancel = cancel
|
||||
}
|
||||
|
||||
static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private var content = ComponentView<Empty>()
|
||||
|
||||
private var component: SaveProgressScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.5))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: SaveProgressScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var animateIn = false
|
||||
var disappearingView: UIView?
|
||||
if let previousComponent, previousComponent.content.type != component.content.type {
|
||||
if let view = self.content.view {
|
||||
disappearingView = view
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
self.content = ComponentView<Empty>()
|
||||
animateIn = true
|
||||
}
|
||||
|
||||
let cornerRadius: CGFloat
|
||||
let content: AnyComponent<Empty>
|
||||
switch component.content {
|
||||
case let .progress(title, progress):
|
||||
content = AnyComponent(ProgressComponent(title: title, value: progress, cancel: component.cancel))
|
||||
cornerRadius = 18.0
|
||||
case let .completion(text):
|
||||
content = AnyComponent(BannerComponent(iconName: "anim_savemedia", text: text))
|
||||
cornerRadius = 9.0
|
||||
}
|
||||
|
||||
let contentSize = self.content.update(
|
||||
transition: transition,
|
||||
component: content,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: 160.0)
|
||||
)
|
||||
let contentFrame = CGRect(
|
||||
origin: .zero,
|
||||
size: contentSize
|
||||
)
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.backgroundView.addSubview(contentView)
|
||||
if animateIn {
|
||||
contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
contentView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: contentView, frame: contentFrame)
|
||||
if let disappearingView {
|
||||
transition.setPosition(view: disappearingView, position: contentFrame.center)
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentFrame.size.width) / 2.0), y: floorToScreenPixels((availableSize.height - contentFrame.size.height) / 2.0)), size: contentFrame.size)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: cornerRadius, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
|
||||
|
||||
final class SaveProgressScreen: ViewController {
|
||||
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
||||
private weak var controller: SaveProgressScreen?
|
||||
private let context: AccountContext
|
||||
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(controller: SaveProgressScreen) {
|
||||
self.controller = controller
|
||||
self.context = controller.context
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .clear
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
self.view.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
}
|
||||
|
||||
private func animateIn() {
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animateScale(from: 0.4, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false)
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
navigationHeight: 0.0,
|
||||
safeInsets: UIEdgeInsets(
|
||||
top: topInset,
|
||||
left: layout.safeInsets.left,
|
||||
bottom: topInset,
|
||||
right: layout.safeInsets.right
|
||||
),
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
metrics: layout.metrics,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
orientation: nil,
|
||||
isVisible: true,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
dateTimeFormat: self.presentationData.dateTimeFormat,
|
||||
controller: { [weak self] in
|
||||
return self?.controller
|
||||
}
|
||||
)
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SaveProgressScreenComponent(
|
||||
context: self.context,
|
||||
content: controller.content,
|
||||
cancel: { [weak self] in
|
||||
if let self, let controller = self.controller {
|
||||
controller.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
},
|
||||
forceUpdate: false,
|
||||
containerSize: layout.size
|
||||
)
|
||||
if let componentView = self.componentHost.view {
|
||||
if componentView.superview == nil {
|
||||
self.view.addSubview(componentView)
|
||||
}
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var node: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
fileprivate let context: AccountContext
|
||||
var content: SaveProgressScreenComponent.Content {
|
||||
didSet {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
self.maybeSetupDismissTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private var dismissTimer: SwiftSignalKit.Timer?
|
||||
|
||||
public var cancelled: () -> Void = {}
|
||||
|
||||
init(context: AccountContext, content: SaveProgressScreenComponent.Content) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.maybeSetupDismissTimer()
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self)
|
||||
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
self.cancelled()
|
||||
|
||||
self.node.animateOut(completion: { [weak self] in
|
||||
if let self {
|
||||
self.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func maybeSetupDismissTimer() {
|
||||
if case .completion = self.content {
|
||||
self.node.isUserInteractionEnabled = false
|
||||
if self.dismissTimer == nil {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
|
||||
if let self {
|
||||
self.node.animateOut(completion: { [weak self] in
|
||||
if let self {
|
||||
self.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
timer.start()
|
||||
self.dismissTimer = timer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
||||
}
|
||||
}
|
@ -51,6 +51,8 @@ swift_library(
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,360 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
import CheckNode
|
||||
import TelegramStringFormatting
|
||||
import AppBundle
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
final class PeerListItemComponent: Component {
|
||||
final class TransitionHint {
|
||||
let synchronousLoad: Bool
|
||||
|
||||
init(synchronousLoad: Bool) {
|
||||
self.synchronousLoad = synchronousLoad
|
||||
}
|
||||
}
|
||||
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool, isTinted: Bool)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let peer: EnginePeer?
|
||||
let subtitle: String?
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat,
|
||||
title: String,
|
||||
peer: EnginePeer?,
|
||||
subtitle: String?,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.sideInset = sideInset
|
||||
self.title = title
|
||||
self.peer = peer
|
||||
self.subtitle = subtitle
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.subtitle != rhs.subtitle {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
if lhs.hasNext != rhs.hasNext {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var iconView: UIImageView?
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var component: PeerListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
var avatarFrame: CGRect {
|
||||
return self.avatarNode.frame
|
||||
}
|
||||
|
||||
var titleFrame: CGRect? {
|
||||
return self.title.view?.frame
|
||||
}
|
||||
|
||||
var labelFrame: CGRect? {
|
||||
guard var value = self.label.view?.frame else {
|
||||
return nil
|
||||
}
|
||||
if let iconView = self.iconView {
|
||||
value.size.width += value.minX - iconView.frame.minX
|
||||
value.origin.x = iconView.frame.minX
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.addSubview(self.containerButton)
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer)
|
||||
}
|
||||
|
||||
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
var synchronousLoad = false
|
||||
if let hint = transition.userData(TransitionHint.self) {
|
||||
synchronousLoad = hint.synchronousLoad
|
||||
}
|
||||
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
var hasSelectionUpdated = false
|
||||
if let previousComponent = self.component {
|
||||
switch previousComponent.selectionState {
|
||||
case .none:
|
||||
if case .none = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
case .editing:
|
||||
if case .editing = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let contextInset: CGFloat = 0.0
|
||||
|
||||
let height: CGFloat = 60.0
|
||||
let verticalInset: CGFloat = 1.0
|
||||
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
if case let .editing(isSelected, isTinted) = component.selectionState {
|
||||
leftInset += 44.0
|
||||
avatarLeftInset += 44.0
|
||||
let checkSize: CGFloat = 22.0
|
||||
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
if themeUpdated {
|
||||
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||
if isTinted {
|
||||
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
|
||||
}
|
||||
checkLayer.theme = theme
|
||||
}
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||
if isTinted {
|
||||
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
|
||||
}
|
||||
checkLayer = CheckLayer(theme: theme)
|
||||
self.checkLayer = checkLayer
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
}
|
||||
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
} else {
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||
checkLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let avatarSize: CGFloat = 40.0
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if self.avatarNode.bounds.isEmpty {
|
||||
self.avatarNode.frame = avatarFrame
|
||||
} else {
|
||||
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||
}
|
||||
if let peer = component.peer {
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
} else {
|
||||
clipStyle = .round
|
||||
}
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
}
|
||||
|
||||
let labelData: (String, Bool)
|
||||
if let subtitle = component.subtitle {
|
||||
labelData = (subtitle, false)
|
||||
} else if case .legacyGroup = component.peer {
|
||||
labelData = (component.strings.Group_Status, false)
|
||||
} else if case let .channel(channel) = component.peer {
|
||||
if case .group = channel.info {
|
||||
labelData = (component.strings.Group_Status, false)
|
||||
} else {
|
||||
labelData = (component.strings.Channel_Status, false)
|
||||
}
|
||||
} else {
|
||||
labelData = (component.strings.Group_Status, false)
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
|
||||
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||
self.addSubview(previousTitleContents)
|
||||
|
||||
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||
previousTitleContents?.removeFromSuperview()
|
||||
})
|
||||
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
if let labelView = self.label.view {
|
||||
var iconLabelOffset: CGFloat = 0.0
|
||||
|
||||
let iconView: UIImageView
|
||||
if let current = self.iconView {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = UIImageView(image: readIconImage)
|
||||
iconView.tintColor = component.theme.list.itemSecondaryTextColor
|
||||
self.iconView = iconView
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
if let image = iconView.image {
|
||||
iconLabelOffset = image.size.width + 4.0
|
||||
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size))
|
||||
}
|
||||
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + iconLabelOffset, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
}
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||
self.separatorLayer.isHidden = !component.hasNext
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -159,6 +159,14 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class ViewList {
|
||||
let externalState = StoryItemSetViewListComponent.ExternalState()
|
||||
let view = ComponentView<Empty>()
|
||||
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
let sendMessageContext: StoryItemSetContainerSendMessage
|
||||
|
||||
@ -184,6 +192,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let footerPanel = ComponentView<Empty>()
|
||||
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||
|
||||
var displayViewList: Bool = false
|
||||
var viewList: ViewList?
|
||||
|
||||
var itemLayout: ItemLayout?
|
||||
var ignoreScrolling: Bool = false
|
||||
|
||||
@ -388,6 +399,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
} else if self.displayReactions {
|
||||
self.displayReactions = false
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
} else if self.displayViewList {
|
||||
self.displayViewList = false
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
} else if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 {
|
||||
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
|
||||
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
@ -485,7 +499,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
|
||||
|
||||
if let view = view as? StoryContentItem.View {
|
||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil)
|
||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
|
||||
}
|
||||
}
|
||||
|
||||
@ -510,7 +524,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
for (_, visibleItem) in self.visibleItems {
|
||||
if let view = visibleItem.view.view {
|
||||
if let view = view as? StoryContentItem.View {
|
||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil)
|
||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -869,6 +883,16 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
component: AnyComponent(StoryFooterPanelComponent(
|
||||
context: component.context,
|
||||
storyItem: currentItem?.storyItem,
|
||||
expandViewStats: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.displayViewList {
|
||||
self.displayViewList = true
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
},
|
||||
deleteAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@ -1053,8 +1077,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
)
|
||||
|
||||
let bottomContentInsetWithoutInput = bottomContentInset
|
||||
var viewListInset: CGFloat = 0.0
|
||||
|
||||
let inputPanelBottomInset: CGFloat
|
||||
var inputPanelBottomInset: CGFloat
|
||||
let inputPanelIsOverlay: Bool
|
||||
if component.inputHeight == 0.0 {
|
||||
inputPanelBottomInset = bottomContentInset
|
||||
@ -1066,9 +1091,81 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
inputPanelIsOverlay = true
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - bottomContentInset))
|
||||
transition.setFrame(view: self.contentContainerView, frame: contentFrame)
|
||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0)
|
||||
if self.displayViewList {
|
||||
let viewList: ViewList
|
||||
var viewListTransition = transition
|
||||
if let current = self.viewList {
|
||||
viewList = current
|
||||
} else {
|
||||
if !transition.animation.isImmediate {
|
||||
viewListTransition = .immediate
|
||||
}
|
||||
viewList = ViewList()
|
||||
self.viewList = viewList
|
||||
}
|
||||
|
||||
let viewListSize = viewList.view.update(
|
||||
transition: viewListTransition,
|
||||
component: AnyComponent(StoryItemSetViewListComponent(
|
||||
externalState: viewList.externalState,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
safeInsets: component.safeInsets,
|
||||
storyItem: component.slice.item.storyItem,
|
||||
close: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.displayViewList = false
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let viewListFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - viewListSize.height), size: viewListSize)
|
||||
if let viewListView = viewList.view.view {
|
||||
var animateIn = false
|
||||
if viewListView.superview == nil {
|
||||
self.addSubview(viewListView)
|
||||
animateIn = true
|
||||
}
|
||||
viewListTransition.setFrame(view: viewListView, frame: viewListFrame)
|
||||
|
||||
if animateIn, !transition.animation.isImmediate {
|
||||
transition.animatePosition(view: viewListView, from: CGPoint(x: 0.0, y: viewListFrame.height), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
viewListInset = viewListFrame.height
|
||||
inputPanelBottomInset = viewListInset
|
||||
} else if let viewList = self.viewList {
|
||||
self.viewList = nil
|
||||
if let viewListView = viewList.view.view {
|
||||
transition.setPosition(view: viewListView, position: CGPoint(x: viewListView.center.x, y: availableSize.height + viewListView.bounds.height * 0.5), completion: { [weak viewListView] _ in
|
||||
viewListView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let contentDefaultBottomInset: CGFloat = bottomContentInset
|
||||
let contentSize = CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - contentDefaultBottomInset)
|
||||
|
||||
let contentVisualBottomInset: CGFloat
|
||||
if self.displayViewList {
|
||||
contentVisualBottomInset = viewListInset + 12.0
|
||||
} else {
|
||||
contentVisualBottomInset = contentDefaultBottomInset
|
||||
}
|
||||
let contentVisualHeight = availableSize.height - component.containerInsets.top - contentVisualBottomInset
|
||||
let contentVisualScale = contentVisualHeight / contentSize.height
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize)
|
||||
|
||||
transition.setPosition(view: self.contentContainerView, position: contentFrame.center)
|
||||
transition.setBounds(view: self.contentContainerView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
transition.setScale(view: self.contentContainerView, scale: contentVisualScale)
|
||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0 * (1.0 / contentVisualScale))
|
||||
|
||||
if self.closeButtonIconView.image == nil {
|
||||
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
|
||||
@ -1078,7 +1175,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let closeButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 50.0, height: 64.0))
|
||||
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
||||
transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size))
|
||||
transition.setAlpha(view: self.closeButton, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: self.closeButton, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let focusedItem: StoryContentItem? = component.slice.item
|
||||
@ -1148,7 +1245,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
view.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1174,13 +1271,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
//view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: view, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: view, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
let gradientHeight: CGFloat = 74.0
|
||||
transition.setFrame(layer: self.topContentGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width, height: gradientHeight)))
|
||||
transition.setAlpha(layer: self.topContentGradientLayer, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
transition.setAlpha(layer: self.topContentGradientLayer, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
|
||||
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
|
||||
self.itemLayout = itemLayout
|
||||
@ -1230,7 +1327,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.addSubview(captionItemView)
|
||||
}
|
||||
captionItemTransition.setFrame(view: captionItemView, frame: captionFrame)
|
||||
captionItemTransition.setAlpha(view: captionItemView, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
captionItemTransition.setAlpha(view: captionItemView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1445,13 +1542,16 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
|
||||
var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize)
|
||||
if self.displayViewList {
|
||||
footerPanelFrame.origin.y += footerPanelSize.height
|
||||
}
|
||||
if let footerPanelView = self.footerPanel.view {
|
||||
if footerPanelView.superview == nil {
|
||||
self.addSubview(footerPanelView)
|
||||
}
|
||||
transition.setFrame(view: footerPanelView, frame: footerPanelFrame)
|
||||
transition.setAlpha(view: footerPanelView, alpha: focusedItem?.isMy == true ? 1.0 : 0.0)
|
||||
transition.setAlpha(view: footerPanelView, alpha: (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
let bottomGradientHeight = inputPanelSize.height + 32.0
|
||||
@ -1464,7 +1564,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
normalDimAlpha = captionItem.externalState.expandFraction
|
||||
}
|
||||
var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha
|
||||
if component.hideUI {
|
||||
if component.hideUI || self.displayViewList {
|
||||
dimAlpha = 0.0
|
||||
}
|
||||
|
||||
@ -1473,9 +1573,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||
let contentSize = availableSize
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
let scrollContentSize = availableSize
|
||||
if scrollContentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = scrollContentSize
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
@ -1505,7 +1605,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.contentContainerView.addSubview(navigationStripView)
|
||||
}
|
||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
||||
transition.setAlpha(view: navigationStripView, alpha: component.hideUI ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: navigationStripView, alpha: (component.hideUI || self.displayViewList) ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
var items: [StoryActionsComponent.Item] = []
|
||||
@ -1542,7 +1642,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if self.displayReactions {
|
||||
inlineActionsAlpha = 0.0
|
||||
}
|
||||
if component.hideUI {
|
||||
if component.hideUI || self.displayViewList {
|
||||
inlineActionsAlpha = 0.0
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,510 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ComponentDisplayAdapters
|
||||
import AccountContext
|
||||
import SwiftSignalKit
|
||||
import TelegramStringFormatting
|
||||
import ShimmerEffect
|
||||
|
||||
final class StoryItemSetViewListComponent: Component {
|
||||
final class ExternalState {
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
let externalState: ExternalState
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let safeInsets: UIEdgeInsets
|
||||
let storyItem: EngineStoryItem
|
||||
let close: () -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
safeInsets: UIEdgeInsets,
|
||||
storyItem: EngineStoryItem,
|
||||
close: @escaping () -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.safeInsets = safeInsets
|
||||
self.storyItem = storyItem
|
||||
self.close = close
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryItemSetViewListComponent, rhs: StoryItemSetViewListComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.safeInsets != rhs.safeInsets {
|
||||
return false
|
||||
}
|
||||
if lhs.storyItem != rhs.storyItem {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
var containerSize: CGSize
|
||||
var bottomInset: CGFloat
|
||||
var topInset: CGFloat
|
||||
var sideInset: CGFloat
|
||||
var itemHeight: CGFloat
|
||||
var itemCount: Int
|
||||
|
||||
var contentSize: CGSize
|
||||
|
||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) {
|
||||
self.containerSize = containerSize
|
||||
self.bottomInset = bottomInset
|
||||
self.topInset = topInset
|
||||
self.sideInset = sideInset
|
||||
self.itemHeight = itemHeight
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow
|
||||
let maxVisibleIndex = maxVisibleRow
|
||||
|
||||
if maxVisibleIndex >= minVisibleIndex {
|
||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func itemFrame(for index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight))
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let navigationBarBackground: BlurredBackgroundView
|
||||
private let navigationSeparator: SimpleLayer
|
||||
private let navigationTitle = ComponentView<Empty>()
|
||||
private let navigationLeftButton = ComponentView<Empty>()
|
||||
|
||||
private let backgroundView: UIView
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var placeholderImage: UIImage?
|
||||
|
||||
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
private var visiblePlaceholderViews: [Int: UIImageView] = [:]
|
||||
|
||||
private var component: StoryItemSetViewListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var viewList: EngineStoryViewListContext?
|
||||
private var viewListDisposable: Disposable?
|
||||
private var viewListState: EngineStoryViewListContext.State?
|
||||
private var requestedLoadMoreToken: EngineStoryViewListContext.LoadMoreToken?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.navigationBarBackground = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.navigationSeparator = SimpleLayer()
|
||||
|
||||
self.backgroundView = UIView()
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.indicatorStyle = .white
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.addSubview(self.navigationBarBackground)
|
||||
self.layer.addSublayer(self.navigationSeparator)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.viewListDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
|
||||
|
||||
var synchronousLoad = false
|
||||
if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) {
|
||||
synchronousLoad = hint.synchronousLoad
|
||||
}
|
||||
|
||||
var validIds: [EnginePeer.Id] = []
|
||||
var validPlaceholderIds: [Int] = []
|
||||
if let range = itemLayout.visibleItems(for: visibleBounds) {
|
||||
for index in range.lowerBound ..< range.upperBound {
|
||||
guard let viewListState = self.viewListState, index < viewListState.totalCount else {
|
||||
continue
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.itemFrame(for: index)
|
||||
|
||||
if index >= viewListState.items.count {
|
||||
validPlaceholderIds.append(index)
|
||||
|
||||
let placeholderView: UIImageView
|
||||
if let current = self.visiblePlaceholderViews[index] {
|
||||
placeholderView = current
|
||||
} else {
|
||||
placeholderView = UIImageView()
|
||||
self.visiblePlaceholderViews[index] = placeholderView
|
||||
self.scrollView.addSubview(placeholderView)
|
||||
|
||||
placeholderView.image = self.placeholderImage
|
||||
}
|
||||
|
||||
placeholderView.frame = itemFrame
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var itemTransition = transition
|
||||
let item = viewListState.items[index]
|
||||
validIds.append(item.peer.id)
|
||||
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.visibleItems[item.peer.id] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
visibleItem = ComponentView()
|
||||
self.visibleItems[item.peer.id] = visibleItem
|
||||
}
|
||||
|
||||
let dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: item.timestamp, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat(
|
||||
dateFormatString: { value in
|
||||
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: [])
|
||||
},
|
||||
tomorrowFormatString: { value in
|
||||
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: [])
|
||||
},
|
||||
todayFormatString: { value in
|
||||
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_TodayAt(value).string, ranges: [])
|
||||
},
|
||||
yesterdayFormatString: { value in
|
||||
return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_YesterdayAt(value).string, ranges: [])
|
||||
}
|
||||
)).string
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
sideInset: itemLayout.sideInset,
|
||||
title: item.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
|
||||
peer: item.peer,
|
||||
subtitle: dateText,
|
||||
selectionState: .none,
|
||||
hasNext: index != viewListState.totalCount - 1,
|
||||
action: { _ in
|
||||
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
var animateIn = false
|
||||
if itemView.superview == nil {
|
||||
animateIn = true
|
||||
self.scrollView.addSubview(itemView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
|
||||
if animateIn, synchronousLoad {
|
||||
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [EnginePeer.Id] = []
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = visibleItem.view {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
var removePlaceholderIds: [Int] = []
|
||||
for (id, placeholderView) in self.visiblePlaceholderViews {
|
||||
if !validPlaceholderIds.contains(id) {
|
||||
removePlaceholderIds.append(id)
|
||||
|
||||
if synchronousLoad {
|
||||
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderView] _ in
|
||||
placeholderView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
placeholderView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removePlaceholderIds {
|
||||
self.visiblePlaceholderViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if let viewList = self.viewList, let viewListState = self.viewListState, visibleBounds.maxY >= self.scrollView.contentSize.height - 200.0 {
|
||||
if self.requestedLoadMoreToken != viewListState.loadMoreToken {
|
||||
self.requestedLoadMoreToken = viewListState.loadMoreToken
|
||||
viewList.loadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StoryItemSetViewListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
let itemUpdated = self.component?.storyItem.id != component.storyItem.id
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: min(availableSize.height, 500.0))
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
|
||||
self.navigationBarBackground.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||
self.navigationSeparator.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
|
||||
}
|
||||
|
||||
if itemUpdated {
|
||||
self.viewListState = nil
|
||||
self.viewList = nil
|
||||
self.viewListDisposable?.dispose()
|
||||
|
||||
if let views = component.storyItem.views {
|
||||
let viewList = component.context.engine.messages.storyViewList(id: component.storyItem.id, views: views)
|
||||
self.viewList = viewList
|
||||
var applyState = false
|
||||
self.viewListDisposable = (viewList.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] listState in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.viewListState = listState
|
||||
if applyState {
|
||||
self.state?.updated(transition: Transition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: true)))
|
||||
}
|
||||
})
|
||||
applyState = true
|
||||
}
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
let navigationHeight: CGFloat = 56.0
|
||||
let navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: navigationHeight))
|
||||
transition.setFrame(view: self.navigationBarBackground, frame: navigationBarFrame)
|
||||
self.navigationBarBackground.update(size: navigationBarFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(layer: self.navigationSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||
|
||||
let navigationLeftButtonSize = self.navigationLeftButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: component.strings.Common_Close, font: Font.regular(17.0), color: component.theme.rootController.navigationBar.accentTextColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.close()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 56.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 120.0, height: 100.0)
|
||||
)
|
||||
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: navigationLeftButtonSize)
|
||||
if let navigationLeftButtonView = self.navigationLeftButton.view {
|
||||
if navigationLeftButtonView.superview == nil {
|
||||
self.addSubview(navigationLeftButtonView)
|
||||
}
|
||||
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
|
||||
}
|
||||
|
||||
let titleText: String
|
||||
|
||||
let viewCount = self.viewListState?.totalCount ?? component.storyItem.views?.seenCount
|
||||
if let viewCount {
|
||||
if viewCount == 1 {
|
||||
titleText = "1 View"
|
||||
} else {
|
||||
titleText = "\(viewCount) Views"
|
||||
}
|
||||
} else {
|
||||
titleText = "No Views"
|
||||
}
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: titleText, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
|
||||
)
|
||||
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - navigationTitleSize.width) * 0.5), y: floor((navigationBarFrame.height - navigationTitleSize.height) * 0.5)), size: navigationTitleSize)
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
if navigationTitleView.superview == nil {
|
||||
self.addSubview(navigationTitleView)
|
||||
}
|
||||
transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center)
|
||||
transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size))
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarFrame.maxY), size: CGSize(width: size.width, height: size.height - navigationBarFrame.maxY)))
|
||||
|
||||
let measureItemSize = self.measureItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
sideInset: sideInset,
|
||||
title: "AAAAAAAAAAAA",
|
||||
peer: nil,
|
||||
subtitle: "BBBBBBB",
|
||||
selectionState: .none,
|
||||
hasNext: true,
|
||||
action: { _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 1000.0)
|
||||
)
|
||||
|
||||
if self.placeholderImage == nil || themeUpdated {
|
||||
self.placeholderImage = generateImage(CGSize(width: 300.0, height: measureItemSize.height), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1).cgColor)
|
||||
|
||||
if let measureItemView = self.measureItem.view as? PeerListItemComponent.View {
|
||||
context.fillEllipse(in: measureItemView.avatarFrame)
|
||||
let lineWidth: CGFloat = 8.0
|
||||
|
||||
if let titleFrame = measureItemView.titleFrame {
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - lineWidth * 0.5)), size: CGSize(width: titleFrame.width, height: lineWidth)), cornerRadius: lineWidth * 0.5).cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
if let labelFrame = measureItemView.labelFrame {
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: labelFrame.minX, y: floor(labelFrame.midY - lineWidth * 0.5)), size: CGSize(width: labelFrame.width, height: lineWidth)), cornerRadius: lineWidth * 0.5).cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
}
|
||||
})?.stretchableImage(withLeftCapWidth: 299, topCapHeight: 0)
|
||||
for (_, placeholderView) in self.visiblePlaceholderViews {
|
||||
placeholderView.image = self.placeholderImage
|
||||
}
|
||||
}
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: size,
|
||||
bottomInset: component.safeInsets.bottom,
|
||||
topInset: 0.0,
|
||||
sideInset: sideInset,
|
||||
itemHeight: measureItemSize.height,
|
||||
itemCount: self.viewListState?.items.count ?? 0
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
let scrollContentSize = itemLayout.contentSize
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
let scrollContentInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
let scrollIndicatorInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: component.safeInsets.bottom, right: 0.0)
|
||||
if self.scrollView.contentInset != scrollContentInsets {
|
||||
self.scrollView.contentInset = scrollContentInsets
|
||||
}
|
||||
if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets
|
||||
}
|
||||
if self.scrollView.contentSize != scrollContentSize {
|
||||
self.scrollView.contentSize = scrollContentSize
|
||||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
private let peerId: EnginePeer.Id
|
||||
|
||||
private(set) var sliceValue: StoryContentContextState.FocusedSlice?
|
||||
fileprivate var nextItems: [EngineStoryItem] = []
|
||||
|
||||
let updated = Promise<Void>()
|
||||
|
||||
@ -154,6 +155,25 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
isPublic: item.isPublic
|
||||
)
|
||||
|
||||
var nextItems: [EngineStoryItem] = []
|
||||
for i in (focusedIndex + 1) ..< min(focusedIndex + 4, itemsView.items.count) {
|
||||
if let item = itemsView.items[i].value.get(Stories.StoredItem.self), case let .item(item) = item, let media = item.media {
|
||||
nextItems.append(EngineStoryItem(
|
||||
id: item.id,
|
||||
timestamp: item.timestamp,
|
||||
media: EngineMedia(media),
|
||||
text: item.text,
|
||||
entities: item.entities,
|
||||
views: nil,
|
||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||
isPinned: item.isPinned,
|
||||
isExpired: item.isExpired,
|
||||
isPublic: item.isPublic
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
self.nextItems = nextItems
|
||||
self.sliceValue = StoryContentContextState.FocusedSlice(
|
||||
peer: peer,
|
||||
item: StoryContentItem(
|
||||
@ -314,6 +334,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
private var requestedStoryKeys = Set<StoryKey>()
|
||||
private var requestStoryDisposables = DisposableSet()
|
||||
|
||||
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
focusedPeerId: EnginePeer.Id?
|
||||
@ -336,6 +358,9 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
deinit {
|
||||
self.storySubscriptionsDisposable?.dispose()
|
||||
self.requestStoryDisposables.dispose()
|
||||
for (_, disposable) in self.preloadStoryResourceDisposables {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePeerContexts() {
|
||||
@ -486,6 +511,82 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
self.statePromise.set(.single(stateValue))
|
||||
|
||||
self.updatedPromise.set(.single(Void()))
|
||||
|
||||
var possibleItems: [(EnginePeer, EngineStoryItem)] = []
|
||||
if let slice = currentState.centralPeerContext.sliceValue {
|
||||
for item in currentState.centralPeerContext.nextItems {
|
||||
possibleItems.append((slice.peer, item))
|
||||
}
|
||||
}
|
||||
if let nextPeerContext = currentState.nextPeerContext, let slice = nextPeerContext.sliceValue {
|
||||
possibleItems.append((slice.peer, slice.item.storyItem))
|
||||
for item in nextPeerContext.nextItems {
|
||||
possibleItems.append((slice.peer, item))
|
||||
}
|
||||
}
|
||||
|
||||
var nextPriority = 0
|
||||
var resultResources: [EngineMediaResource.Id: StoryPreloadInfo] = [:]
|
||||
for i in 0 ..< min(possibleItems.count, 3) {
|
||||
let peer = possibleItems[i].0
|
||||
let item = possibleItems[i].1
|
||||
if let peerReference = PeerReference(peer._asPeer()) {
|
||||
if let image = item.media._asMedia() as? TelegramMediaImage, let resource = image.representations.last?.resource {
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: image), resource: resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
} else if let file = item.media._asMedia() as? TelegramMediaFile {
|
||||
if let preview = file.previewRepresentations.last {
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: preview.resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: nil,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
}
|
||||
|
||||
let resource = MediaResourceReference.media(media: .story(peer: peerReference, id: item.id, media: file), resource: file.resource)
|
||||
resultResources[EngineMediaResource.Id(resource.resource.id)] = StoryPreloadInfo(
|
||||
resource: resource,
|
||||
size: file.preloadSize,
|
||||
priority: .top(position: nextPriority)
|
||||
)
|
||||
nextPriority += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validIds: [MediaResourceId] = []
|
||||
for (_, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) {
|
||||
let resource = info.resource
|
||||
validIds.append(resource.resource.id)
|
||||
if self.preloadStoryResourceDisposables[resource.resource.id] == nil {
|
||||
var fetchRange: (Range<Int64>, MediaBoxFetchPriority)?
|
||||
if let size = info.size {
|
||||
fetchRange = (0 ..< Int64(size), .default)
|
||||
}
|
||||
#if DEBUG
|
||||
fetchRange = nil
|
||||
#endif
|
||||
self.preloadStoryResourceDisposables[resource.resource.id] = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource, range: fetchRange).start()
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [MediaResourceId] = []
|
||||
for (id, disposable) in self.preloadStoryResourceDisposables {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.preloadStoryResourceDisposables.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
public func resetSideStates() {
|
||||
|
@ -169,13 +169,19 @@ final class StoryItemContentComponent: Component {
|
||||
self.videoNode = videoNode
|
||||
self.addSubnode(videoNode)
|
||||
|
||||
videoNode.playbackCompleted = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.environment?.presentationProgressUpdated(1.0)
|
||||
}
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if value {
|
||||
self.videoNode?.seek(0.0)
|
||||
self.videoNode?.playOnceWithSound(playAndRecord: false)
|
||||
self.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
|
||||
}
|
||||
}
|
||||
videoNode.canAttachContent = true
|
||||
@ -404,9 +410,7 @@ final class StoryItemContentComponent: Component {
|
||||
wasSynchronous = false
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
self.performActionAfterImageContentLoaded(update: false)
|
||||
#endif
|
||||
|
||||
self.fetchDisposable?.dispose()
|
||||
self.fetchDisposable = nil
|
||||
|
@ -12,17 +12,20 @@ import TelegramCore
|
||||
public final class StoryFooterPanelComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let storyItem: EngineStoryItem?
|
||||
public let expandViewStats: () -> Void
|
||||
public let deleteAction: () -> Void
|
||||
public let moreAction: (UIView, ContextGesture?) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
storyItem: EngineStoryItem?,
|
||||
expandViewStats: @escaping () -> Void,
|
||||
deleteAction: @escaping () -> Void,
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.storyItem = storyItem
|
||||
self.expandViewStats = expandViewStats
|
||||
self.deleteAction = deleteAction
|
||||
self.moreAction = moreAction
|
||||
}
|
||||
@ -38,6 +41,7 @@ public final class StoryFooterPanelComponent: Component {
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let viewStatsButton: HighlightableButton
|
||||
private let viewStatsText = ComponentView<Empty>()
|
||||
private let deleteButton = ComponentView<Empty>()
|
||||
private var moreButton: MoreHeaderButton?
|
||||
@ -49,18 +53,31 @@ public final class StoryFooterPanelComponent: Component {
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.viewStatsButton = HighlightableButton()
|
||||
|
||||
self.avatarsContext = AnimatedAvatarSetContext()
|
||||
self.avatarsNode = AnimatedAvatarSetNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.avatarsNode.view)
|
||||
self.avatarsNode.view.isUserInteractionEnabled = false
|
||||
self.viewStatsButton.addSubview(self.avatarsNode.view)
|
||||
self.addSubview(self.viewStatsButton)
|
||||
|
||||
self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func viewStatsPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.expandViewStats()
|
||||
}
|
||||
|
||||
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
@ -85,16 +102,22 @@ public final class StoryFooterPanelComponent: Component {
|
||||
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
||||
}
|
||||
|
||||
let viewsText: String
|
||||
var viewCount = 0
|
||||
if let views = component.storyItem?.views, views.seenCount != 0 {
|
||||
if views.seenCount == 1 {
|
||||
viewsText = "1 view"
|
||||
} else {
|
||||
viewsText = "\(views.seenCount) views"
|
||||
}
|
||||
} else {
|
||||
viewsText = "No views yet"
|
||||
viewCount = views.seenCount
|
||||
}
|
||||
|
||||
let viewsText: String
|
||||
if viewCount == 0 {
|
||||
viewsText = "No Views"
|
||||
} else if viewCount == 1 {
|
||||
viewsText = "1 view"
|
||||
} else {
|
||||
viewsText = "\(viewCount) views"
|
||||
}
|
||||
|
||||
self.viewStatsButton.isEnabled = viewCount != 0
|
||||
|
||||
let viewStatsTextSize = self.viewStatsText.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)),
|
||||
@ -105,12 +128,15 @@ public final class StoryFooterPanelComponent: Component {
|
||||
if let viewStatsTextView = self.viewStatsText.view {
|
||||
if viewStatsTextView.superview == nil {
|
||||
viewStatsTextView.layer.anchorPoint = CGPoint()
|
||||
self.addSubview(viewStatsTextView)
|
||||
viewStatsTextView.isUserInteractionEnabled = false
|
||||
self.viewStatsButton.addSubview(viewStatsTextView)
|
||||
}
|
||||
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
|
||||
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size))
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: viewStatsTextFrame.maxX, height: viewStatsTextFrame.maxY + 8.0)))
|
||||
|
||||
var rightContentOffset: CGFloat = availableSize.width - 12.0
|
||||
|
||||
let deleteButtonSize = self.deleteButton.update(
|
||||
|
@ -19,6 +19,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -25,6 +25,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let collapseFraction: CGFloat
|
||||
public let uploadProgress: Float?
|
||||
public let peerAction: (EnginePeer?) -> Void
|
||||
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@ -34,7 +35,8 @@ public final class StoryPeerListComponent: Component {
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
collapseFraction: CGFloat,
|
||||
uploadProgress: Float?,
|
||||
peerAction: @escaping (EnginePeer?) -> Void
|
||||
peerAction: @escaping (EnginePeer?) -> Void,
|
||||
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -44,6 +46,7 @@ public final class StoryPeerListComponent: Component {
|
||||
self.collapseFraction = collapseFraction
|
||||
self.uploadProgress = uploadProgress
|
||||
self.peerAction = peerAction
|
||||
self.contextPeerAction = contextPeerAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
|
||||
@ -319,7 +322,8 @@ public final class StoryPeerListComponent: Component {
|
||||
collapsedWidth: collapsedItemWidth,
|
||||
leftNeighborDistance: leftNeighborDistance,
|
||||
rightNeighborDistance: rightNeighborDistance,
|
||||
action: component.peerAction
|
||||
action: component.peerAction,
|
||||
contextGesture: component.contextPeerAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: itemLayout.itemSize
|
||||
|
@ -9,6 +9,8 @@ import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
import ContextUI
|
||||
import AsyncDisplayKit
|
||||
|
||||
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
|
||||
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
|
||||
@ -152,6 +154,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
public let leftNeighborDistance: CGFloat?
|
||||
public let rightNeighborDistance: CGFloat?
|
||||
public let action: (EnginePeer) -> Void
|
||||
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@ -165,7 +168,8 @@ public final class StoryPeerListItemComponent: Component {
|
||||
collapsedWidth: CGFloat,
|
||||
leftNeighborDistance: CGFloat?,
|
||||
rightNeighborDistance: CGFloat?,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -179,6 +183,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.leftNeighborDistance = leftNeighborDistance
|
||||
self.rightNeighborDistance = rightNeighborDistance
|
||||
self.action = action
|
||||
self.contextGesture = contextGesture
|
||||
}
|
||||
|
||||
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
|
||||
@ -218,7 +223,13 @@ public final class StoryPeerListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: HighlightTrackingButton {
|
||||
public final class View: UIView {
|
||||
private let extractedContainerNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let extractedBackgroundView: UIImageView
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
|
||||
private let avatarContainer: UIView
|
||||
private var avatarNode: AvatarNode?
|
||||
private var avatarAddBadgeView: UIImageView?
|
||||
@ -233,6 +244,13 @@ public final class StoryPeerListItemComponent: Component {
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.extractedBackgroundView = UIImageView()
|
||||
self.extractedBackgroundView.alpha = 0.0
|
||||
|
||||
self.avatarContainer = UIView()
|
||||
self.avatarContainer.isUserInteractionEnabled = false
|
||||
|
||||
@ -248,9 +266,16 @@ public final class StoryPeerListItemComponent: Component {
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.avatarContainer)
|
||||
self.extractedContainerNode.contentNode.view.addSubview(self.extractedBackgroundView)
|
||||
|
||||
self.layer.addSublayer(self.indicatorColorLayer)
|
||||
self.containerNode.addSubnode(self.extractedContainerNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
|
||||
self.addSubview(self.containerNode.view)
|
||||
|
||||
self.extractedContainerNode.contentNode.view.addSubview(self.button)
|
||||
self.button.addSubview(self.avatarContainer)
|
||||
|
||||
self.button.layer.addSublayer(self.indicatorColorLayer)
|
||||
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
|
||||
self.indicatorColorLayer.mask = self.indicatorMaskLayer
|
||||
|
||||
@ -262,7 +287,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.indicatorShapeLayer.lineWidth = 2.0
|
||||
self.indicatorShapeLayer.lineCap = .round
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -274,7 +299,36 @@ public final class StoryPeerListItemComponent: Component {
|
||||
self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25)
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.button.isEnabled = false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.button.isEnabled = true
|
||||
}
|
||||
component.contextGesture(self.extractedContainerNode, gesture, component.peer)
|
||||
}
|
||||
|
||||
self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
if isExtracted {
|
||||
self.extractedBackgroundView.image = generateStretchableFilledCircleImage(diameter: 24.0, color: component.theme.contextMenu.backgroundColor)
|
||||
}
|
||||
transition.updateAlpha(layer: self.extractedBackgroundView.layer, alpha: isExtracted ? 1.0 : 0.0, completion: { [weak self] _ in
|
||||
if !isExtracted {
|
||||
self?.extractedBackgroundView.image = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
@ -293,11 +347,24 @@ public final class StoryPeerListItemComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let size = availableSize
|
||||
|
||||
transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setFrame(view: self.extractedBackgroundView, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -4.0, dy: -4.0))
|
||||
|
||||
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundView.frame.minX - 2.0, y: self.extractedBackgroundView.frame.minY), size: CGSize(width: self.extractedBackgroundView.frame.width + 4.0, height: self.extractedBackgroundView.frame.height))
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let hadUnseen = self.component?.hasUnseen
|
||||
let hadProgress = self.component?.progress != nil
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
let previousComponent = self.component
|
||||
|
||||
self.containerNode.isGestureEnabled = component.peer.id != component.context.account.peerId
|
||||
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
@ -464,7 +531,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
if titleView.superview == nil {
|
||||
titleView.layer.anchorPoint = CGPoint()
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
self.button.addSubview(titleView)
|
||||
}
|
||||
titleTransition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
|
@ -260,7 +260,7 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
|
||||
if let mediaEditorValues {
|
||||
let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, frameRate: 30.0)
|
||||
let videoExport = MediaEditorVideoExport(account: account, subject: .video(avAsset), configuration: configuration, outputPath: tempFile.path)
|
||||
videoExport.startExport()
|
||||
videoExport.start()
|
||||
|
||||
let statusDisposable = videoExport.status.start(next: { status in
|
||||
switch status {
|
||||
@ -293,6 +293,8 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
|
||||
EngineTempBox.shared.dispose(tempFile)
|
||||
case .failed:
|
||||
subscriber.putError(.generic)
|
||||
case let .progress(progress):
|
||||
subscriber.putNext(.progressUpdated(progress))
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -414,7 +416,7 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo
|
||||
}
|
||||
|
||||
let videoExport = MediaEditorVideoExport(account: account, subject: subject, configuration: configuration, outputPath: tempFile.path)
|
||||
videoExport.startExport()
|
||||
videoExport.start()
|
||||
|
||||
let statusDisposable = videoExport.status.start(next: { status in
|
||||
switch status {
|
||||
@ -447,6 +449,8 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo
|
||||
EngineTempBox.shared.dispose(tempFile)
|
||||
case .failed:
|
||||
subscriber.putError(.generic)
|
||||
case let .progress(progress):
|
||||
subscriber.putNext(.progressUpdated(progress))
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -292,8 +292,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return nil
|
||||
case let .image(image):
|
||||
return .image(image, PixelDimensions(image.size))
|
||||
case let .video(path, dimensions):
|
||||
return .video(path, dimensions)
|
||||
case let .video(path, transitionImage, dimensions):
|
||||
return .video(path, transitionImage, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
@ -342,7 +342,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
|
||||
if let chatListController = self.chatListController as? ChatListControllerImpl {
|
||||
chatListController.scrollToTop?()
|
||||
chatListController.scrollToStories()
|
||||
switch mediaResult {
|
||||
case let .image(image, dimensions, caption):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user