Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-06-03 10:25:20 +04:00
commit 0833da0e0a
45 changed files with 2666 additions and 341 deletions

View File

@ -10,6 +10,7 @@ private final class CameraContext {
private let device: CameraDevice private let device: CameraDevice
private let input = CameraInput() private let input = CameraInput()
private let output = CameraOutput() private let output = CameraOutput()
private let cameraImageContext = CIContext()
private let initialConfiguration: Camera.Configuration private let initialConfiguration: Camera.Configuration
private var invalidated = false private var invalidated = false
@ -40,14 +41,18 @@ private final class CameraContext {
} }
} }
private let previewSnapshotContext = CIContext()
private var lastSnapshotTimestamp: Double = CACurrentMediaTime() private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) { private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool) {
Queue.concurrentDefaultQueue().async { Queue.concurrentDefaultQueue().async {
var ciImage = CIImage(cvImageBuffer: pixelBuffer) var ciImage = CIImage(cvImageBuffer: pixelBuffer)
let size = ciImage.extent.size 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)) 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) let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
CameraSimplePreviewView.saveLastStateImage(uiImage) CameraSimplePreviewView.saveLastStateImage(uiImage)
} }
@ -78,27 +83,13 @@ private final class CameraContext {
let timestamp = CACurrentMediaTime() let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5 { 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 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 self.output.processFaceLandmarks = { [weak self] observations in
@ -240,7 +231,7 @@ private final class CameraContext {
return self.output.startRecording() return self.output.startRecording()
} }
public func stopRecording() -> Signal<String?, NoError> { public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
return self.output.stopRecording() 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 return Signal { subscriber in
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.queue.async { self.queue.async {

View File

@ -1,5 +1,8 @@
import Foundation
import AVFoundation import AVFoundation
import UIKit
import SwiftSignalKit import SwiftSignalKit
import CoreImage
import Vision import Vision
import VideoToolbox 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> { func startRecording() -> Signal<Double, NoError> {
guard self.videoRecorder == nil else { guard self.videoRecorder == nil else {
return .complete() return .complete()
@ -184,14 +187,13 @@ final class CameraOutput: NSObject {
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4" let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
let outputFileURL = URL(fileURLWithPath: outputFilePath) 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 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 { if case let .success(transitionImage) = result {
self?.recordingCompletionPipe.putNext(outputFilePath) self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage))
} else { } else {
self?.recordingCompletionPipe.putNext(nil) self?.recordingCompletionPipe.putNext(nil)
} }
}) })
videoRecorder?.start() videoRecorder?.start()
self.videoRecorder = videoRecorder 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() self.videoRecorder?.stop()
return self.recordingCompletionPipe.signal() return self.recordingCompletionPipe.signal()

View File

@ -1,5 +1,7 @@
import Foundation import Foundation
import AVFoundation import AVFoundation
import UIKit
import CoreImage
import SwiftSignalKit import SwiftSignalKit
import TelegramCore import TelegramCore
@ -32,6 +34,10 @@ private final class VideoRecorderImpl {
private var videoInput: AVAssetWriterInput? private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput?
private let imageContext: CIContext
private var transitionImage: UIImage?
private var savedTransitionImage = false
private var pendingAudioSampleBuffers: [CMSampleBuffer] = [] private var pendingAudioSampleBuffers: [CMSampleBuffer] = []
private var _duration: CMTime = .zero private var _duration: CMTime = .zero
@ -46,7 +52,7 @@ private final class VideoRecorderImpl {
private let configuration: VideoRecorder.Configuration private let configuration: VideoRecorder.Configuration
private let videoTransform: CGAffineTransform private let videoTransform: CGAffineTransform
private let url: URL private let url: URL
fileprivate var completion: (Bool) -> Void = { _ in } fileprivate var completion: (Bool, UIImage?) -> Void = { _, _ in }
private let error = Atomic<Error?>(value: nil) private let error = Atomic<Error?>(value: nil)
@ -58,6 +64,7 @@ private final class VideoRecorderImpl {
self.configuration = configuration self.configuration = configuration
self.videoTransform = videoTransform self.videoTransform = videoTransform
self.url = fileUrl self.url = fileUrl
self.imageContext = CIContext()
try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: url)
guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else { guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else {
@ -91,7 +98,6 @@ private final class VideoRecorderImpl {
guard !self.stopped && self.error.with({ $0 }) == nil else { guard !self.stopped && self.error.with({ $0 }) == nil else {
return return
} }
var failed = false var failed = false
if self.videoInput == nil { if self.videoInput == nil {
let videoSettings = self.configuration.videoSettings let videoSettings = self.configuration.videoSettings
@ -139,6 +145,17 @@ private final class VideoRecorderImpl {
} }
if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData { 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) { if videoInput.append(sampleBuffer) {
self.lastVideoSampleTime = presentationTime self.lastVideoSampleTime = presentationTime
let startTime = self.recordingStartSampleTime let startTime = self.recordingStartSampleTime
@ -274,21 +291,21 @@ private final class VideoRecorderImpl {
let completion = self.completion let completion = self.completion
if self.recordingStopSampleTime == .invalid { if self.recordingStopSampleTime == .invalid {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(false) completion(false, nil)
} }
return return
} }
if let _ = self.error.with({ $0 }) { if let _ = self.error.with({ $0 }) {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(false) completion(false, nil)
} }
return return
} }
if !self.tryAppendingPendingAudioBuffers() { if !self.tryAppendingPendingAudioBuffers() {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(false) completion(false, nil)
} }
return return
} }
@ -297,21 +314,21 @@ private final class VideoRecorderImpl {
self.assetWriter.finishWriting { self.assetWriter.finishWriting {
if let _ = self.assetWriter.error { if let _ = self.assetWriter.error {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(false) completion(false, nil)
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(true) completion(true, self.transitionImage)
} }
} }
} }
} else if let _ = self.assetWriter.error { } else if let _ = self.assetWriter.error {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(true) completion(false, nil)
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
completion(true) completion(false, nil)
} }
} }
} }
@ -390,7 +407,7 @@ public final class VideoRecorder {
case generic case generic
} }
case success case success(UIImage?)
case initError(Error) case initError(Error)
case writeError(Error) case writeError(Error)
case finishError(Error) case finishError(Error)
@ -431,10 +448,10 @@ public final class VideoRecorder {
return nil return nil
} }
self.impl = impl self.impl = impl
impl.completion = { [weak self] success in impl.completion = { [weak self] result, transitionImage in
if let self { if let self {
if success { if result {
self.completion(.success) self.completion(.success(transitionImage))
} else { } else {
self.completion(.finishError(.generic)) self.completion(.finishError(.generic))
} }

View File

@ -1848,6 +1848,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let size = info.size { if let size = info.size {
fetchRange = (0 ..< Int64(size), .default) 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() 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) 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 stateAndFilterId.state.editing {
if case .chatList(.root) = self.location { if case .chatList(.root) = self.location {
self.rightButton = nil self.rightButton = nil
self.storyButton = nil
} }
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle 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 { } else if isReorderingTabs {
if case .chatList(.root) = self.location { if case .chatList(.root) = self.location {
self.rightButton = nil self.rightButton = nil
self.storyButton = nil
} }
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true), 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 { } else {
self.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( self.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Edit, isBold: false), content: .text(title: presentationData.strings.Common_Edit, isBold: false),
@ -5473,18 +5533,6 @@ private final class ChatListLocationContext {
self.proxyButton = nil 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 self.chatListTitle = titleContent
if case .chatList(.root) = self.location, checkProxy { if case .chatList(.root) = self.location, checkProxy {

View File

@ -914,35 +914,31 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if itemNode.listNode.isTracking { if itemNode.listNode.isTracking {
if case let .known(value) = offset { if case let .known(value) = offset {
if !self.storiesUnlocked { if !self.storiesUnlocked {
if value < -1.0 { if value < -50.0 {
self.storiesUnlocked = true self.storiesUnlocked = true
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { guard let self else {
return return
} }
HapticFeedback().impact()
self.currentItemNode.ignoreStoryInsetAdjustment = true self.currentItemNode.ignoreStoryInsetAdjustment = true
self.currentItemNode.allowInsetFixWhileTracking = true
self.onStoriesLockedUpdated?(true) self.onStoriesLockedUpdated?(true)
self.currentItemNode.ignoreStoryInsetAdjustment = false self.currentItemNode.ignoreStoryInsetAdjustment = false
self.currentItemNode.allowInsetFixWhileTracking = false
} }
} }
} }
} }
} else { } else if self.storiesUnlocked {
switch offset { switch offset {
case let .known(value): case let .known(value):
if value >= 94.0 { if value >= 94.0 {
if self.storiesUnlocked {
self.storiesUnlocked = false self.storiesUnlocked = false
self.currentItemNode.stopScrolling()
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.onStoriesLockedUpdated?(false) self.onStoriesLockedUpdated?(false)
} }
}
}
default: default:
break break
} }
@ -957,7 +953,6 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if value > 94.0 { if value > 94.0 {
if self.storiesUnlocked { if self.storiesUnlocked {
self.storiesUnlocked = false self.storiesUnlocked = false
self.currentItemNode.stopScrolling()
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { guard let self else {
@ -1720,7 +1715,8 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
guard let self else { guard let self else {
return 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 let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in

View File

@ -1214,6 +1214,8 @@ public final class ChatListNode: ListView {
super.init() super.init()
self.useMainQueueTransactions = true
self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor self.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor
self.verticalScrollIndicatorFollowsOverscroll = true self.verticalScrollIndicatorFollowsOverscroll = true
@ -3128,6 +3130,7 @@ public final class ChatListNode: ListView {
} }
var options = transition.options var options = transition.options
options.insert(.Synchronous)
if self.view.window != nil { if self.view.window != nil {
if !options.contains(.AnimateInsertion) { if !options.contains(.AnimateInsertion) {
options.insert(.PreferSynchronousDrawing) options.insert(.PreferSynchronousDrawing)

View File

@ -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) { public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) {
if view.bounds == bounds { if view.bounds == bounds {
completion?(true) completion?(true)

View File

@ -15,6 +15,7 @@ swift_library(
"//submodules/AppBundle:AppBundle", "//submodules/AppBundle:AppBundle",
"//submodules/Display:Display", "//submodules/Display:Display",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/GZip:GZip",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -4,6 +4,7 @@ import Lottie
import AppBundle import AppBundle
import HierarchyTrackingLayer import HierarchyTrackingLayer
import Display import Display
import GZip
public final class LottieAnimationComponent: Component { public final class LottieAnimationComponent: Component {
public struct AnimationItem: Equatable { public struct AnimationItem: Equatable {
@ -176,7 +177,14 @@ public final class LottieAnimationComponent: Component {
self.didPlayToCompletion = false self.didPlayToCompletion = false
self.currentCompletion = nil 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)) let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
switch component.animation.mode { switch component.animation.mode {
case .still, .animateTransitionFromPrevious: case .still, .animateTransitionFromPrevious:

View File

@ -206,6 +206,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
public final var dynamicBounceEnabled = true public final var dynamicBounceEnabled = true
public final var rotated = false public final var rotated = false
public final var experimentalSnapScrollToItem = false public final var experimentalSnapScrollToItem = false
public final var useMainQueueTransactions = false
public final var scrollEnabled: Bool = true { public final var scrollEnabled: Bool = true {
didSet { didSet {
@ -250,6 +251,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
} }
public final var snapToBottomInsetUntilFirstInteraction: Bool = false public final var snapToBottomInsetUntilFirstInteraction: Bool = false
public final var allowInsetFixWhileTracking: Bool = false
public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)? public final var updateFloatingHeaderOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)? public final var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)?
@ -595,8 +597,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
action() action()
} }
}*/ }*/
if self.useMainQueueTransactions && Thread.isMainThread {
action()
} else {
DispatchQueue.main.async(execute: action) DispatchQueue.main.async(execute: action)
} }
}
private func beginReordering(itemNode: ListViewItemNode) { private func beginReordering(itemNode: ListViewItemNode) {
self.isReordering = true self.isReordering = true
@ -980,7 +986,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.trackingOffset += -deltaY self.trackingOffset += -deltaY
} }
if self.useMainQueueTransactions {
DispatchQueue.main.async { [weak self] in
self?.enqueueUpdateVisibleItems(synchronous: false)
}
} else {
self.enqueueUpdateVisibleItems(synchronous: false) self.enqueueUpdateVisibleItems(synchronous: false)
}
var useScrollDynamics = false var useScrollDynamics = false
@ -1630,19 +1642,29 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let wasIgnoringScrollingEvents = self.ignoreScrollingEvents let wasIgnoringScrollingEvents = self.ignoreScrollingEvents
self.ignoreScrollingEvents = true self.ignoreScrollingEvents = true
if topItemFound && bottomItemFound { if topItemFound && bottomItemFound {
if self.scroller.contentSize != CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0) {
self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight) self.scroller.contentSize = CGSize(width: self.visibleSize.width, height: completeHeight)
}
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset self.scroller.contentOffset = self.lastContentOffset
}
} else if topItemFound { } else if topItemFound {
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.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
}
self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge) self.lastContentOffset = CGPoint(x: 0.0, y: -topItemEdge)
if self.scroller.contentOffset != self.lastContentOffset { if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset self.scroller.contentOffset = self.lastContentOffset
} }
} else if bottomItemFound { } else if bottomItemFound {
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.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
}
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge) self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize * 2.0 - bottomItemEdge)
if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset self.scroller.contentOffset = self.lastContentOffset
}
} else if self.itemNodes.isEmpty { } else if self.itemNodes.isEmpty {
self.scroller.contentSize = self.visibleSize self.scroller.contentSize = self.visibleSize
if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero { if self.lastContentOffset.y == infiniteScrollSize && self.scroller.contentOffset.y.isZero {
@ -1650,10 +1672,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.lastContentOffset = .zero self.lastContentOffset = .zero
} }
} else { } else {
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.scroller.contentSize = CGSize(width: self.visibleSize.width, height: infiniteScrollSize * 2.0)
}
if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 { if abs(self.scroller.contentOffset.y - infiniteScrollSize) > infiniteScrollSize / 2.0 {
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize) self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
if self.scroller.contentOffset != self.lastContentOffset {
self.scroller.contentOffset = self.lastContentOffset self.scroller.contentOffset = self.lastContentOffset
}
} else { } else {
self.lastContentOffset = self.scroller.contentOffset self.lastContentOffset = self.scroller.contentOffset
} }
@ -1662,8 +1688,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
private func async(_ f: @escaping () -> Void) { private func async(_ f: @escaping () -> Void) {
if self.useMainQueueTransactions {
if Thread.isMainThread {
f()
} else {
DispatchQueue.main.async(execute: f)
}
} else {
DispatchQueue.global(qos: .userInteractive).async(execute: f) DispatchQueue.global(qos: .userInteractive).async(execute: f)
//DispatchQueue.main.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) { 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 var offsetFix: CGFloat
let insetDeltaOffsetFix: CGFloat = 0.0 let insetDeltaOffsetFix: CGFloat = 0.0
if self.isTracking || isExperimentalSnapToScrollToItem { if (self.isTracking && !self.allowInsetFixWhileTracking) || isExperimentalSnapToScrollToItem {
offsetFix = 0.0 offsetFix = 0.0
} else if self.snapToBottomInsetUntilFirstInteraction { } else if self.snapToBottomInsetUntilFirstInteraction {
offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom

View File

@ -1450,7 +1450,7 @@ open class NavigationBar: ASDisplayNode {
if let titleView = titleView as? NavigationBarTitleView { if let titleView = titleView as? NavigationBarTitleView {
let titleWidth = size.width - (leftTitleInset > 0.0 ? leftTitleInset : rightTitleInset) - (rightTitleInset > 0.0 ? rightTitleInset : leftTitleInset) 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 { if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar {

View File

@ -4,5 +4,5 @@ import UIKit
public protocol NavigationBarTitleView { public protocol NavigationBarTitleView {
func animateLayoutTransition() func animateLayoutTransition()
func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect
} }

View File

@ -329,7 +329,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} }
view.containerView = self 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) let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
if snapped { if snapped {
self.insertSubview(snapView, belowSubview: view) self.insertSubview(snapView, belowSubview: view)
@ -348,20 +351,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} }
switch type { switch type {
case .centerX: case .centerX:
processSnap(snapped: snapped, snapView: self.xAxisView) processSnap(snapped, self.xAxisView)
case .centerY: case .centerY:
processSnap(snapped: snapped, snapView: self.yAxisView) processSnap(snapped, self.yAxisView)
case .top: case .top:
processSnap(snapped: snapped, snapView: self.topEdgeView) processSnap(snapped, self.topEdgeView)
self.edgePreviewUpdated(snapped) self.edgePreviewUpdated(snapped)
case .left: case .left:
processSnap(snapped: snapped, snapView: self.leftEdgeView) processSnap(snapped, self.leftEdgeView)
self.edgePreviewUpdated(snapped) self.edgePreviewUpdated(snapped)
case .right: case .right:
processSnap(snapped: snapped, snapView: self.rightEdgeView) processSnap(snapped, self.rightEdgeView)
self.edgePreviewUpdated(snapped) self.edgePreviewUpdated(snapped)
case .bottom: case .bottom:
processSnap(snapped: snapped, snapView: self.bottomEdgeView) processSnap(snapped, self.bottomEdgeView)
self.edgePreviewUpdated(snapped) self.edgePreviewUpdated(snapped)
case let .rotation(angle): case let .rotation(angle):
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)

View File

@ -41,7 +41,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView {
self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) 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 leftInset: CGFloat = 0.0
let rightInset: 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) 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() { func animateLayoutTransition() {

View File

@ -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.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) 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 { 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() super.layoutSubviews()
if let (size, clearBounds) = self.validLayout { 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) self.validLayout = (size, clearBounds)
let titleSize = self.titleNode.updateLayout(size) let titleSize = self.titleNode.updateLayout(size)
@ -661,7 +661,7 @@ private final class ItemListTextWithSubtitleTitleView: UIView, NavigationBarTitl
self.titleNode.frame = titleFrame self.titleNode.frame = titleFrame
self.subtitleNode.frame = subtitleFrame self.subtitleNode.frame = subtitleFrame
return titleSize.width return titleFrame
} }
func animateLayoutTransition() { func animateLayoutTransition() {

View File

@ -413,7 +413,10 @@ final class FFMpegMediaFrameSourceContext: NSObject {
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) 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) 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 fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) 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) audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0)
break break

View File

@ -389,7 +389,8 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
self.fileMap.serialize(manager: self.manager, to: self.metaPath) self.fileMap.serialize(manager: self.manager, to: self.metaPath)
} }
case let .progressUpdated(progress): case let .progressUpdated(progress):
let _ = progress self.fileMap.progressUpdated(progress)
self.updateStatusRequests()
case let .replaceHeader(data, range): case let .replaceHeader(data, range):
self.processWrite(resourceOffset: 0, data: data, dataRange: range) self.processWrite(resourceOffset: 0, data: data, dataRange: range)
case let .moveLocalFile(path): case let .moveLocalFile(path):
@ -576,7 +577,11 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
updatedStatus = .Remote(progress: progress) updatedStatus = .Remote(progress: progress)
} }
} else if self.pendingFetch != nil { } else if self.pendingFetch != nil {
if let progress = self.fileMap.progress {
updatedStatus = .Fetching(isActive: true, progress: progress)
} else {
updatedStatus = .Fetching(isActive: true, progress: 0.0) updatedStatus = .Fetching(isActive: true, progress: 0.0)
}
} else { } else {
updatedStatus = .Remote(progress: 0.0) updatedStatus = .Remote(progress: 0.0)
} }

View File

@ -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 { 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 contextResult: OutgoingChatContextResultMessageAttribute?
var autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute? var autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?
var autoclearMessageAttribute: AutoclearTimeoutMessageAttribute? 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) return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)), .text)
} else if let contextResult = contextResult { } else if let contextResult = contextResult {
return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .chatContextResult(contextResult), reuploadInfo: nil, cacheReferenceKey: nil)), .text) 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) return .signal(mediaResult, .media)
} else { } else {
return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text) 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 let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
if peerId.namespace == Namespaces.Peer.SecretChat, let resource = largest.resource as? SecretFileMediaResource { 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))) 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 { } else {
if forceReupload { if forceReupload {
let mediaReference: AnyMediaReference 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))) 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 { } 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 { } else if let contact = media as? TelegramMediaContact {
let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "") 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 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) return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, forceRefresh: forceReupload)
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in |> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
var referenceKey: CachedSentMediaReferenceKey? var referenceKey: CachedSentMediaReferenceKey?
@ -694,8 +694,19 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
} else { } else {
fileReference = .standalone(media: file) 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) let upload: Signal<MultipartUploadResult?, PendingMessageUploadError> = .single(nil)
|> mapError { _ -> PendingMessageUploadError in return .generic |> 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 var alreadyTransformed = false
for attribute in attributes { for attribute in attributes {
@ -774,10 +785,20 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
} }
}) })
return combineLatest(upload, transformedFileAndThumbnail) return combineLatest(upload, transformedFileAndThumbnail, resourceStatus)
|> mapToSignal { content, fileAndThumbnailResult -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in |> 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 { switch content {
case let .progress(progress): case let .progress(progress):
var progress = progress
if passFetchProgress {
progress = 0.33 + progress * 0.67
}
return .single(.progress(progress)) return .single(.progress(progress))
case let .inputFile(inputFile): case let .inputFile(inputFile):
if case let .done(file, thumbnail) = fileAndThumbnailResult { if case let .done(file, thumbnail) = fileAndThumbnailResult {

View File

@ -59,7 +59,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
case let .update(media): case let .update(media):
let generateUploadSignal: (Bool) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? = { forceReupload in let generateUploadSignal: (Bool) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? = { forceReupload in
let augmentedMedia = augmentMediaWithReference(media) 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) { if let uploadSignal = generateUploadSignal(forceReupload) {
uploadedMedia = .single(.progress(0.027)) uploadedMedia = .single(.progress(0.027))

View File

@ -521,6 +521,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
revalidationContext: account.mediaReferenceRevalidationContext, revalidationContext: account.mediaReferenceRevalidationContext,
forceReupload: true, forceReupload: true,
isGrouped: false, isGrouped: false,
passFetchProgress: false,
peerId: account.peerId, peerId: account.peerId,
messageId: nil, messageId: nil,
attributes: [], attributes: [],
@ -553,6 +554,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
revalidationContext: account.mediaReferenceRevalidationContext, revalidationContext: account.mediaReferenceRevalidationContext,
forceReupload: true, forceReupload: true,
isGrouped: false, isGrouped: false,
passFetchProgress: true,
peerId: account.peerId, peerId: account.peerId,
messageId: nil, messageId: nil,
attributes: [], 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()
}
}
}

View File

@ -878,5 +878,9 @@ public extension TelegramEngine {
public func getStoryViewList(account: Account, id: Int32, offsetTimestamp: Int32?, offsetPeerId: PeerId?, limit: Int) -> Signal<StoryViewList?, NoError> { 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) 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)
}
} }
} }

View File

@ -252,9 +252,9 @@ private final class CameraScreenComponent: CombinedComponent {
func stopVideoRecording() { func stopVideoRecording() {
self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0) self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0)
self.resultDisposable.set((self.camera.stopRecording() self.resultDisposable.set((self.camera.stopRecording()
|> deliverOnMainQueue).start(next: { [weak self] path in |> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in
if let self, let path { if let self, let (path, transitionImage) = pathAndTransitionImage {
self.completion.invoke(.single(.video(path, PixelDimensions(width: 1080, height: 1920)))) self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920))))
} }
})) }))
self.updated(transition: .spring(duration: 0.4)) self.updated(transition: .spring(duration: 0.4))
@ -641,7 +641,7 @@ public class CameraScreen: ViewController {
public enum Result { public enum Result {
case pendingImage case pendingImage
case image(UIImage) case image(UIImage)
case video(String, PixelDimensions) case video(String, UIImage?, PixelDimensions)
case asset(PHAsset) case asset(PHAsset)
case draft(MediaEditorDraft) case draft(MediaEditorDraft)
} }

View File

@ -299,6 +299,7 @@ public final class ChatListHeaderComponent: Component {
var contentOffsetFraction: CGFloat = 0.0 var contentOffsetFraction: CGFloat = 0.0
private(set) var centerContentWidth: CGFloat = 0.0 private(set) var centerContentWidth: CGFloat = 0.0
private(set) var centerContentOffsetX: CGFloat = 0.0
init( init(
backPressed: @escaping () -> Void, 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.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)) 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 { if let chatListTitle = content.chatListTitle {
var chatListTitleTransition = transition var chatListTitleTransition = transition
let chatListTitleView: ChatListTitleView let chatListTitleView: ChatListTitleView
@ -639,8 +642,13 @@ public final class ChatListHeaderComponent: Component {
chatListTitleView.theme = theme chatListTitleView.theme = theme
chatListTitleView.strings = strings chatListTitleView.strings = strings
chatListTitleView.setTitle(chatListTitle, animated: false) chatListTitleView.setTitle(chatListTitle, animated: false)
let centerContentWidth = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), sideContentWidth: sideContentWidth, transition: transition.containedViewLayoutTransition) let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition)
self.centerContentWidth = centerContentWidth 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 chatListTitleView.openStatusSetup = { [weak self] sourceView in
guard let self else { guard let self else {
@ -655,7 +663,14 @@ public final class ChatListHeaderComponent: Component {
self.toggleIsLocked() 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 { } else {
if let chatListTitleView = self.chatListTitleView { if let chatListTitleView = self.chatListTitleView {
self.chatListTitleView = nil self.chatListTitleView = nil
@ -664,6 +679,8 @@ public final class ChatListHeaderComponent: Component {
} }
self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil 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 let storyPeerListExternalState = StoryPeerListComponent.ExternalState()
private var storyPeerList: ComponentView<Empty>? private var storyPeerList: ComponentView<Empty>?
public var storyPeerAction: ((EnginePeer?) -> Void)? public var storyPeerAction: ((EnginePeer?) -> Void)?
public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)?
private var effectiveContentView: ContentView? { private var effectiveContentView: ContentView? {
return self.secondaryContentView ?? self.primaryContentView return self.secondaryContentView ?? self.primaryContentView
@ -795,9 +813,6 @@ public final class ChatListHeaderComponent: Component {
self.storyPeerList = storyPeerList self.storyPeerList = storyPeerList
} }
if let uploadProgress = component.uploadProgress {
print("out \(uploadProgress)")
}
let _ = storyPeerList.update( let _ = storyPeerList.update(
transition: storyListTransition, transition: storyListTransition,
component: AnyComponent(StoryPeerListComponent( component: AnyComponent(StoryPeerListComponent(
@ -813,10 +828,16 @@ public final class ChatListHeaderComponent: Component {
return return
} }
self.storyPeerAction?(peer) self.storyPeerAction?(peer)
},
contextPeerAction: { [weak self] sourceNode, gesture, peer in
guard let self else {
return
}
self.storyContextPeerAction?(sourceNode, gesture, peer)
} }
)), )),
environment: {}, 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)) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition) primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition)
@ -900,7 +921,7 @@ public final class ChatListHeaderComponent: Component {
self.secondaryContentView = secondaryContentView self.secondaryContentView = secondaryContentView
self.addSubview(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)) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition)
@ -956,7 +977,7 @@ public final class ChatListHeaderComponent: Component {
var defaultStoryListX: CGFloat = 0.0 var defaultStoryListX: CGFloat = 0.0
if let primaryContentView = self.primaryContentView { 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))) 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)))

View File

@ -143,7 +143,9 @@ public final class ChatListNavigationBar: Component {
override public init(frame: CGRect) { override public init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0)
self.separatorLayer = SimpleLayer() self.separatorLayer = SimpleLayer()
self.separatorLayer.anchorPoint = CGPoint()
super.init(frame: frame) super.init(frame: frame)
@ -173,10 +175,7 @@ public final class ChatListNavigationBar: Component {
} }
public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) { public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) {
var transition = transition let transition = transition
if self.applyScrollFractionAnimator != nil {
transition = .immediate
}
self.rawScrollOffset = offset self.rawScrollOffset = offset
@ -217,9 +216,13 @@ public final class ChatListNavigationBar: Component {
let previousHeight = self.backgroundView.bounds.height let previousHeight = self.backgroundView.bounds.height
self.backgroundView.update(size: visibleSize, transition: transition.containedViewLayoutTransition) self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), 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))) 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 let searchContentNode: NavigationBarSearchContentNode
if let current = self.searchContentNode { if let current = self.searchContentNode {
@ -253,6 +256,7 @@ public final class ChatListNavigationBar: Component {
component.activateSearch(searchContentNode) component.activateSearch(searchContentNode)
} }
) )
searchContentNode.view.layer.anchorPoint = CGPoint()
self.searchContentNode = searchContentNode self.searchContentNode = searchContentNode
self.addSubview(searchContentNode.view) self.addSubview(searchContentNode.view)
} }
@ -279,11 +283,17 @@ public final class ChatListNavigationBar: Component {
let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance
searchContentNode.expansionProgress = 1.0 - searchOffsetFraction 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) 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( let headerContentSize = self.headerContent.update(
transition: transition, transition: headerTransition,
component: AnyComponent(ChatListHeaderComponent( component: AnyComponent(ChatListHeaderComponent(
sideInset: component.sideInset + 16.0, sideInset: component.sideInset + 16.0,
primaryContent: component.primaryContent, primaryContent: component.primaryContent,
@ -325,9 +335,10 @@ public final class ChatListNavigationBar: Component {
let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize) let headerContentFrame = CGRect(origin: CGPoint(x: 0.0, y: headerContentY), size: headerContentSize)
if let headerContentView = self.headerContent.view { if let headerContentView = self.headerContent.view {
if headerContentView.superview == nil { if headerContentView.superview == nil {
headerContentView.layer.anchorPoint = CGPoint()
self.addSubview(headerContentView) self.addSubview(headerContentView)
} }
transition.setFrame(view: headerContentView, frame: headerContentFrame) transition.setFrameWithAdditivePosition(view: headerContentView, frame: headerContentFrame)
} }
if component.tabsNode !== self.tabsNode { 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)) 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 { 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 { if let tabsNode = component.tabsNode {
@ -357,6 +369,7 @@ public final class ChatListNavigationBar: Component {
var tabsNodeTransition = transition var tabsNodeTransition = transition
if tabsNode.view.superview !== self { if tabsNode.view.superview !== self {
tabsNode.view.layer.anchorPoint = CGPoint()
tabsNodeTransition = .immediate tabsNodeTransition = .immediate
self.addSubview(tabsNode.view) self.addSubview(tabsNode.view)
if !transition.animation.isImmediate { 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)
} }
} }

View File

@ -62,7 +62,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
public var openStatusSetup: ((UIView) -> Void)? public var openStatusSetup: ((UIView) -> Void)?
private var validLayout: (CGSize, CGRect, CGFloat)? private var validLayout: (CGSize, CGRect)?
public var manualLayout: Bool = false public var manualLayout: Bool = false
@ -316,13 +316,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
override public func layoutSubviews() { override public func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, clearBounds, sideContentWidth) self.validLayout = (size, clearBounds)
var indicatorPadding: CGFloat = 0.0 var indicatorPadding: CGFloat = 0.0
let indicatorSize = self.activityIndicator.bounds.size let indicatorSize = self.activityIndicator.bounds.size
@ -344,9 +344,9 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
let combinedHeight = titleSize.height 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) 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() { @objc private func buttonPressed() {

View File

@ -118,7 +118,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
private let button: HighlightTrackingButtonNode private let button: HighlightTrackingButtonNode
var manualLayout: Bool = false var manualLayout: Bool = false
private var validLayout: (CGSize, CGRect, CGFloat)? private var validLayout: (CGSize, CGRect)?
private var titleLeftIcon: ChatTitleIcon = .none private var titleLeftIcon: ChatTitleIcon = .none
private var titleRightIcon: ChatTitleIcon = .none private var titleRightIcon: ChatTitleIcon = .none
@ -355,8 +355,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.button.isUserInteractionEnabled = isEnabled self.button.isUserInteractionEnabled = isEnabled
if !self.updateStatus() { if !self.updateStatus() {
if updated { if updated {
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.2, curve: .easeInOut)) 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.activityNode.transitionToState(state, animation: .slide) {
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .animated(duration: 0.3, curve: .spring)) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring))
} }
return true return true
} else { } else {
@ -688,8 +688,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
override public func layoutSubviews() { override public func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
@ -704,14 +704,14 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
self.titleContent = titleContent self.titleContent = titleContent
let _ = self.updateStatus() let _ = self.updateStatus()
if !self.manualLayout, let (size, clearBounds, sideContentWidth) = self.validLayout { if !self.manualLayout, let (size, clearBounds) = self.validLayout {
let _ = self.updateLayout(size: size, clearBounds: clearBounds, sideContentWidth: sideContentWidth, transition: .immediate) let _ = self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate)
} }
} }
} }
public func updateLayout(size: CGSize, clearBounds: CGRect, sideContentWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) -> CGRect {
self.validLayout = (size, clearBounds, sideContentWidth) self.validLayout = (size, clearBounds)
self.button.frame = clearBounds self.button.frame = clearBounds
self.contentContainer.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))) 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() { @objc private func buttonPressed() {
@ -1015,7 +1015,7 @@ public final class ChatTitleComponent: Component {
} }
contentView.updateThemeAndStrings(theme: component.theme, strings: component.strings, hasEmbeddedTitleContent: false) 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)) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize return availableSize

View File

@ -23,13 +23,13 @@ public struct MediaEditorPlayerState {
public final class MediaEditor { public final class MediaEditor {
public enum Subject { public enum Subject {
case image(UIImage, PixelDimensions) case image(UIImage, PixelDimensions)
case video(String, PixelDimensions) case video(String, UIImage?, PixelDimensions)
case asset(PHAsset) case asset(PHAsset)
case draft(MediaEditorDraft) case draft(MediaEditorDraft)
var dimensions: PixelDimensions { var dimensions: PixelDimensions {
switch self { switch self {
case let .image(_, dimensions), let .video(_, dimensions): case let .image(_, dimensions), let .video(_, _, dimensions):
return dimensions return dimensions
case let .asset(asset): case let .asset(asset):
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
@ -189,7 +189,7 @@ public final class MediaEditor {
let duration = asset.duration.seconds let duration = asset.duration.seconds
let interval = duration / Double(count) let interval = duration / Double(count)
for i in 0 ..< 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] = [] var updatedFrames: [UIImage] = []
@ -287,41 +287,50 @@ public final class MediaEditor {
colors = mediaEditorGetGradientColors(from: image) colors = mediaEditorGetGradientColors(from: image)
} }
textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1)) 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 textureSource = Signal { subscriber in
let url = URL(fileURLWithPath: path) let url = URL(fileURLWithPath: path)
let asset = AVURLAsset(url: url) 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 playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem) 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 { if let image {
let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image)) let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image))
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
} else { } else {
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black)) subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black))
} }
subscriber.putCompletion()
} }
return ActionDisposable { return ActionDisposable {
imageGenerator.cancelAllCGImageGeneration() imageGenerator.cancelAllCGImageGeneration()
} }
} }
}
case let .asset(asset): case let .asset(asset):
textureSource = Signal { subscriber in textureSource = Signal { subscriber in
if asset.mediaType == .video { 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 { if let image {
var degraded = false
if let info { if let info {
if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled {
return return
} }
if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue {
degraded = true
} }
}
if !degraded {
let colors = mediaEditorGetGradientColors(from: image) let colors = mediaEditorGetGradientColors(from: image)
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in
if let asset { if let asset {
@ -332,7 +341,6 @@ public final class MediaEditor {
} }
}) })
} }
}
}) })
return ActionDisposable { return ActionDisposable {
PHImageManager.default().cancelImageRequest(requestId) PHImageManager.default().cancelImageRequest(requestId)
@ -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 self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
if let self { if let self {
let start = self.values.videoTrimRange?.lowerBound ?? 0.0 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() self.player?.play()
} }
}) })
@ -449,9 +457,10 @@ public final class MediaEditor {
if !play { if !play {
self.player?.pause() 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 { if self.targetTimePosition?.0 != targetPosition {
self.targetTimePosition = (targetPosition, play) self.targetTimePosition = (targetPosition, play)
print("targetchange")
if !self.updatingTimePosition { if !self.updatingTimePosition {
self.updateVideoTimePosition() self.updateVideoTimePosition()
} }
@ -474,8 +483,10 @@ public final class MediaEditor {
return return
} }
self.updatingTimePosition = true 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 { if let self {
print("done")
if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition { if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition {
self.updatingTimePosition = false self.updatingTimePosition = false
self.targetTimePosition = nil self.targetTimePosition = nil
@ -486,22 +497,12 @@ public final class MediaEditor {
}) })
} }
public func setVideoTrimStart(_ trimStart: Double) { public func setVideoTrimRange(_ trimRange: Range<Double>) {
self.skipRendering = true self.skipRendering = true
let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0
let trimRange = trimStart ..< trimEnd
self.values = self.values.withUpdatedVideoTrimRange(trimRange) self.values = self.values.withUpdatedVideoTrimRange(trimRange)
self.skipRendering = false self.skipRendering = false
}
public func setVideoTrimEnd(_ trimEnd: Double) { self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000))
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]) { public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) {

View File

@ -101,7 +101,7 @@ final class MediaEditorComposer {
} }
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) 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]) { 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)) ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))

View File

@ -7,7 +7,7 @@ import SwiftSignalKit
protocol TextureConsumer: AnyObject { protocol TextureConsumer: AnyObject {
func consumeTexture(_ texture: MTLTexture, render: Bool) 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 { final class RenderingContext {
@ -247,14 +247,20 @@ 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) let _ = self.semaphore.wait(timeout: .distantFuture)
self.currentPixelBuffer = (pixelBuffer, rotation) self.currentPixelBuffer = (pixelBuffer, rotation)
if render { if render {
if self.previousPresentationTimestamp == timestamp {
self.semaphore.signal()
} else {
self.renderFrame() self.renderFrame()
} }
} }
self.previousPresentationTimestamp = timestamp
}
func renderTargetDidChange(_ target: RenderTarget?) { func renderTargetDidChange(_ target: RenderTarget?) {
self.renderTarget = target self.renderTarget = target

View File

@ -198,7 +198,7 @@ public final class MediaEditorVideoExport {
var timeRange: CMTimeRange? { var timeRange: CMTimeRange? {
if let videoTrimRange = self.values.videoTrimRange { 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 { } else {
return nil return nil
} }
@ -236,7 +236,7 @@ public final class MediaEditorVideoExport {
public enum ExportStatus { public enum ExportStatus {
case unknown case unknown
case progress(Double) case progress(Float)
case completed case completed
case failed(ExportError) case failed(ExportError)
} }
@ -259,6 +259,13 @@ public final class MediaEditorVideoExport {
private var textureRotation: TextureRotation = .rotate0Degrees private var textureRotation: TextureRotation = .rotate0Degrees
private let duration = ValuePromise<CMTime>() private let duration = ValuePromise<CMTime>()
private var durationValue: CMTime? {
didSet {
if let durationValue = self.durationValue {
self.duration.set(durationValue)
}
}
}
private let pauseDispatchGroup = DispatchGroup() private let pauseDispatchGroup = DispatchGroup()
private var cancelled = false private var cancelled = false
@ -279,14 +286,14 @@ public final class MediaEditorVideoExport {
private func setup() { private func setup() {
if case let .video(asset) = self.subject { if case let .video(asset) = self.subject {
if let trimmedVideoDuration = self.configuration.timeRange?.duration { if let trimmedVideoDuration = self.configuration.timeRange?.duration {
self.duration.set(trimmedVideoDuration) self.durationValue = trimmedVideoDuration
} else { } else {
asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"]) { asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"]) {
self.duration.set(asset.duration) self.durationValue = asset.duration
} }
} }
} else { } else {
self.duration.set(CMTime(seconds: 5, preferredTimescale: 1)) self.durationValue = CMTime(seconds: 5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
} }
switch self.subject { switch self.subject {
@ -325,20 +332,23 @@ public final class MediaEditorVideoExport {
let videoTracks = asset.tracks(withMediaType: .video) let videoTracks = asset.tracks(withMediaType: .video)
if (videoTracks.count > 0) { if (videoTracks.count > 0) {
var sourceFrameRate: Float = 0.0 var sourceFrameRate: Float = 0.0
let outputSettings: [String: Any] = [ let colorProperties: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
AVVideoColorPropertiesKey: [
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
] ]
let outputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelBufferMetalCompatibilityKey as String: true,
AVVideoColorPropertiesKey: colorProperties
] ]
if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing { if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing {
} else { } else {
self.setupComposer() self.setupComposer()
} }
let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings) let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings)
videoOutput.alwaysCopiesSampleData = false videoOutput.alwaysCopiesSampleData = true
if reader.canAdd(videoOutput) { if reader.canAdd(videoOutput) {
reader.add(videoOutput) reader.add(videoOutput)
} else { } else {
@ -519,8 +529,13 @@ public final class MediaEditorVideoExport {
} }
self.pauseDispatchGroup.wait() self.pauseDispatchGroup.wait()
if let buffer = output.copyNextSampleBuffer() { if let buffer = output.copyNextSampleBuffer() {
if let composer = self.composer {
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) 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 {
composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in
if let pixelBuffer { if let pixelBuffer {
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
@ -595,6 +610,12 @@ public final class MediaEditorVideoExport {
self.resume() self.resume()
} }
self.cancelled = true self.cancelled = true
self.queue.async {
if let reader = self.reader, reader.status == .reading {
reader.cancelReading()
}
}
} }
private let statusPromise = Promise<ExportStatus>(.unknown) private let statusPromise = Promise<ExportStatus>(.unknown)
@ -607,7 +628,6 @@ public final class MediaEditorVideoExport {
return self.statusPromise.get() return self.statusPromise.get()
} }
private func startImageVideoExport() { private func startImageVideoExport() {
guard self.internalStatus == .idle, let writer = self.writer else { guard self.internalStatus == .idle, let writer = self.writer else {
self.statusValue = .failed(.invalid) self.statusValue = .failed(.invalid)
@ -687,7 +707,7 @@ public final class MediaEditorVideoExport {
} }
} }
public func startExport() { public func start() {
switch self.subject { switch self.subject {
case .video: case .video:
self.startVideoExport() self.startVideoExport()

View File

@ -127,7 +127,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
] ]
let output = AVPlayerItemVideoOutput(outputSettings: outputSettings) let output = AVPlayerItemVideoOutput(outputSettings: outputSettings)
output.suppressesPlayerRendering = true //output.suppressesPlayerRendering = true
output.setDelegate(self, queue: self.queue) output.setDelegate(self, queue: self.queue)
playerItem.add(output) playerItem.add(output)
self.playerItemOutput = output self.playerItemOutput = output
@ -163,7 +163,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
if self.player.rate != 0 { if self.player.rate != 0 {
self.forceUpdate = true self.forceUpdate = true
} }
self.update(forced: self.forceUpdate) self.update(forced: true) //self.forceUpdate)
self.forceUpdate = false self.forceUpdate = false
} }
@ -186,7 +186,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
var presentationTime: CMTime = .zero var presentationTime: CMTime = .zero
if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) { 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)
} }
} }

View File

@ -612,8 +612,7 @@ final class MediaEditorScreenComponent: Component {
framesUpdateTimestamp: playerState.framesUpdateTimestamp, framesUpdateTimestamp: playerState.framesUpdateTimestamp,
trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in
if let mediaEditor { if let mediaEditor {
mediaEditor.setVideoTrimStart(start) mediaEditor.setVideoTrimRange(start..<end)
mediaEditor.setVideoTrimEnd(end)
if done { if done {
mediaEditor.seek(start, andPlay: true) mediaEditor.seek(start, andPlay: true)
} else { } else {
@ -1440,7 +1439,7 @@ public final class MediaEditorScreen: ViewController {
} }
} else if abs(translation.x) > 10.0 && !self.isDismissing { } else if abs(translation.x) > 10.0 && !self.isDismissing {
self.isEnhacing = true self.isEnhacing = true
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
} }
if self.isDismissing { if self.isDismissing {
@ -1467,7 +1466,7 @@ public final class MediaEditorScreen: ViewController {
} }
} else { } else {
self.isEnhacing = false self.isEnhacing = false
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut))
} }
default: default:
break break
@ -1500,23 +1499,15 @@ public final class MediaEditorScreen: ViewController {
} }
} }
func animateIn() { private func setupTransitionImage(_ image: UIImage) {
if let transitionIn = self.controller?.transitionIn {
switch transitionIn {
case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera)
}
case let .gallery(transitionIn):
if let sourceImage = transitionIn.sourceImage {
self.previewContainerView.alpha = 1.0 self.previewContainerView.alpha = 1.0
let transitionInView = UIImageView(image: sourceImage) let transitionInView = UIImageView(image: image)
var initialScale: CGFloat var initialScale: CGFloat
if sourceImage.size.height > sourceImage.size.width { if image.size.height > image.size.width {
initialScale = max(self.previewContainerView.bounds.width / sourceImage.size.width, self.previewContainerView.bounds.height / sourceImage.size.height) initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height)
} else { } else {
initialScale = self.previewContainerView.bounds.width / sourceImage.size.width 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.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0)
transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale) transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale)
@ -1530,6 +1521,21 @@ public final class MediaEditorScreen: ViewController {
} }
} }
} }
func animateIn() {
if let transitionIn = self.controller?.transitionIn {
switch transitionIn {
case .camera:
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.setupTransitionImage(sourceImage)
}
if let sourceView = transitionIn.sourceView { if let sourceView = transitionIn.sourceView {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .gallery) view.animateIn(from: .gallery)
@ -1714,34 +1720,62 @@ public final class MediaEditorScreen: ViewController {
self.controller?.present(tooltipController, in: .current) self.controller?.present(tooltipController, in: .current)
} }
private var saveTooltip: TooltipScreen? private weak var saveTooltip: SaveProgressScreen?
func presentSaveTooltip() { func presentSaveTooltip() {
guard let controller = self.controller, let sourceView = self.componentHost.findTaggedView(tag: saveButtonTag) else { guard let controller = self.controller else {
return return
} }
if let saveTooltip = self.saveTooltip { if let saveTooltip = self.saveTooltip {
saveTooltip.dismiss(animated: true) if case .completion = saveTooltip.content {
saveTooltip.dismiss()
self.saveTooltip = nil 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 text: String
let isVideo = self.mediaEditor?.resultIsVideo ?? false let isVideo = self.mediaEditor?.resultIsVideo ?? false
if isVideo { if isVideo {
text = "Video saved to Photos" text = "Video saved to Photos."
} else { } 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 if let tooltipController = self.saveTooltip {
return .ignore tooltipController.content = .completion(text)
}) } else {
self.saveTooltip = tooltipController let tooltipController = SaveProgressScreen(context: self.context, content: .completion(text))
controller.present(tooltipController, in: .current) 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? private weak var storyArchiveTooltip: ViewController?
@ -2009,13 +2043,13 @@ public final class MediaEditorScreen: ViewController {
public enum Subject { public enum Subject {
case image(UIImage, PixelDimensions) case image(UIImage, PixelDimensions)
case video(String, PixelDimensions) case video(String, UIImage?, PixelDimensions)
case asset(PHAsset) case asset(PHAsset)
case draft(MediaEditorDraft) case draft(MediaEditorDraft)
var dimensions: PixelDimensions { var dimensions: PixelDimensions {
switch self { switch self {
case let .image(_, dimensions), let .video(_, dimensions): case let .image(_, dimensions), let .video(_, _, dimensions):
return dimensions return dimensions
case let .asset(asset): case let .asset(asset):
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
@ -2028,8 +2062,8 @@ public final class MediaEditorScreen: ViewController {
switch self { switch self {
case let .image(image, dimensions): case let .image(image, dimensions):
return .image(image, dimensions) return .image(image, dimensions)
case let .video(videoPath, dimensions): case let .video(videoPath, transitionImage, dimensions):
return .video(videoPath, dimensions) return .video(videoPath, transitionImage, dimensions)
case let .asset(asset): case let .asset(asset):
return .asset(asset) return .asset(asset)
case let .draft(draft): case let .draft(draft):
@ -2041,7 +2075,7 @@ public final class MediaEditorScreen: ViewController {
switch self { switch self {
case let .image(image, dimensions): case let .image(image, dimensions):
return .image(image, dimensions) return .image(image, dimensions)
case let .video(videoPath, dimensions): case let .video(videoPath, _, dimensions):
return .video(videoPath, dimensions) return .video(videoPath, dimensions)
case let .asset(asset): case let .asset(asset):
return .asset(asset) return .asset(asset)
@ -2096,6 +2130,10 @@ public final class MediaEditorScreen: ViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.exportDisposable.dispose()
}
override public func loadDisplayNode() { override public func loadDisplayNode() {
self.displayNode = Node(controller: self) self.displayNode = Node(controller: self)
@ -2273,6 +2311,10 @@ public final class MediaEditorScreen: ViewController {
} }
func maybePresentDiscardAlert() { 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 { if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false {
self.requestDismiss(saveDraft: false, animated: true) self.requestDismiss(saveDraft: false, animated: true)
return return
@ -2397,7 +2439,7 @@ public final class MediaEditorScreen: ViewController {
} }
videoResult = .imageFile(path: tempImagePath) videoResult = .imageFile(path: tempImagePath)
duration = 5.0 duration = 5.0
case let .video(path, _): case let .video(path, _, _):
videoResult = .videoFile(path: path) videoResult = .videoFile(path: path)
if let videoTrimRange = mediaEditor.values.videoTrimRange { if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound 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.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 self?.node.animateOut(finished: true, completion: { [weak self] in
finished()
self?.dismiss() self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
}) })
}) })
@ -2444,8 +2488,10 @@ public final class MediaEditorScreen: ViewController {
if let resultImage { if let resultImage {
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in 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 self?.node.animateOut(finished: true, completion: { [weak self] in
finished()
self?.dismiss() self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
}) })
}) })
if case let .draft(draft) = subject { if case let .draft(draft) = subject {
@ -2458,7 +2504,7 @@ public final class MediaEditorScreen: ViewController {
} }
private var videoExport: MediaEditorVideoExport? private var videoExport: MediaEditorVideoExport?
private var exportDisposable: Disposable? private var exportDisposable = MetaDisposable()
private var previousSavedValues: MediaEditorValues? private var previousSavedValues: MediaEditorValues?
func requestSave() { func requestSave() {
@ -2497,9 +2543,12 @@ public final class MediaEditorScreen: ViewController {
} }
if mediaEditor.resultIsVideo { if mediaEditor.resultIsVideo {
mediaEditor.stop()
self.node.entitiesView.pause()
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError> let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject { switch subject {
case let .video(path, _): case let .video(path, _, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .single(.video(asset)) exportSubject = .single(.video(asset))
case let .image(image, _): 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) let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath)
self.videoExport = videoExport self.videoExport = videoExport
videoExport.startExport() videoExport.start()
self.exportDisposable = (videoExport.status self.exportDisposable.set((videoExport.status
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
if let self { if let self {
if case .completed = status { switch status {
case .completed:
self.videoExport = nil self.videoExport = nil
saveToPhotos(outputPath, true) saveToPhotos(outputPath, true)
self.node.presentSaveTooltip() 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 { } else {
if let image = mediaEditor.resultImage { if let image = mediaEditor.resultImage {
Queue.concurrentDefaultQueue().async {
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in 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) { if let data = resultImage?.jpegData(compressionQuality: 0.8) {
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg" let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
try? data.write(to: URL(fileURLWithPath: outputPath)) try? data.write(to: URL(fileURLWithPath: outputPath))
Queue.mainQueue().async {
saveToPhotos(outputPath, false) saveToPhotos(outputPath, false)
} }
}
}) })
}
self.node.presentSaveTooltip() 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() { private func dismissAllTooltips() {
self.window?.forEachController({ controller in self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
@ -2588,6 +2668,9 @@ public final class MediaEditorScreen: ViewController {
if let controller = controller as? TooltipScreen { if let controller = controller as? TooltipScreen {
controller.dismiss() controller.dismiss()
} }
if let controller = controller as? SaveProgressScreen {
controller.dismiss()
}
return true return true
}) })
} }

View File

@ -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))
}
}

View File

@ -51,6 +51,8 @@ swift_library(
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/ChatPresentationInterfaceState", "//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramStringFormatting",
"//submodules/ShimmerEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -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)
}
}

View File

@ -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 { public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let sendMessageContext: StoryItemSetContainerSendMessage let sendMessageContext: StoryItemSetContainerSendMessage
@ -184,6 +192,9 @@ public final class StoryItemSetContainerComponent: Component {
let footerPanel = ComponentView<Empty>() let footerPanel = ComponentView<Empty>()
let inputPanelExternalState = MessageInputPanelComponent.ExternalState() let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
var displayViewList: Bool = false
var viewList: ViewList?
var itemLayout: ItemLayout? var itemLayout: ItemLayout?
var ignoreScrolling: Bool = false var ignoreScrolling: Bool = false
@ -388,6 +399,9 @@ public final class StoryItemSetContainerComponent: Component {
} else if self.displayReactions { } else if self.displayReactions {
self.displayReactions = false self.displayReactions = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) 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 { } else if let captionItem = self.captionItem, captionItem.externalState.expandFraction > 0.0 {
if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View { if let captionItemView = captionItem.view.view as? StoryContentCaptionComponent.View {
captionItemView.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) 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)) itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size))
if let view = view as? StoryContentItem.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)
} }
} }
@ -510,7 +524,7 @@ public final class StoryItemSetContainerComponent: Component {
for (_, visibleItem) in self.visibleItems { for (_, visibleItem) in self.visibleItems {
if let view = visibleItem.view.view { if let view = visibleItem.view.view {
if let view = view as? StoryContentItem.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( component: AnyComponent(StoryFooterPanelComponent(
context: component.context, context: component.context,
storyItem: currentItem?.storyItem, 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 deleteAction: { [weak self] in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
@ -1053,8 +1077,9 @@ public final class StoryItemSetContainerComponent: Component {
) )
let bottomContentInsetWithoutInput = bottomContentInset let bottomContentInsetWithoutInput = bottomContentInset
var viewListInset: CGFloat = 0.0
let inputPanelBottomInset: CGFloat var inputPanelBottomInset: CGFloat
let inputPanelIsOverlay: Bool let inputPanelIsOverlay: Bool
if component.inputHeight == 0.0 { if component.inputHeight == 0.0 {
inputPanelBottomInset = bottomContentInset inputPanelBottomInset = bottomContentInset
@ -1066,9 +1091,81 @@ public final class StoryItemSetContainerComponent: Component {
inputPanelIsOverlay = true 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)) if self.displayViewList {
transition.setFrame(view: self.contentContainerView, frame: contentFrame) let viewList: ViewList
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0) 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 { if self.closeButtonIconView.image == nil {
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate) 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)) 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.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.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 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) 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) //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 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.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)) let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
self.itemLayout = itemLayout self.itemLayout = itemLayout
@ -1230,7 +1327,7 @@ public final class StoryItemSetContainerComponent: Component {
self.addSubview(captionItemView) self.addSubview(captionItemView)
} }
captionItemTransition.setFrame(view: captionItemView, frame: captionFrame) 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 let footerPanelView = self.footerPanel.view {
if footerPanelView.superview == nil { if footerPanelView.superview == nil {
self.addSubview(footerPanelView) self.addSubview(footerPanelView)
} }
transition.setFrame(view: footerPanelView, frame: footerPanelFrame) 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 let bottomGradientHeight = inputPanelSize.height + 32.0
@ -1464,7 +1564,7 @@ public final class StoryItemSetContainerComponent: Component {
normalDimAlpha = captionItem.externalState.expandFraction normalDimAlpha = captionItem.externalState.expandFraction
} }
var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha var dimAlpha: CGFloat = (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : normalDimAlpha
if component.hideUI { if component.hideUI || self.displayViewList {
dimAlpha = 0.0 dimAlpha = 0.0
} }
@ -1473,9 +1573,9 @@ public final class StoryItemSetContainerComponent: Component {
self.ignoreScrolling = true 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))) 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 let scrollContentSize = availableSize
if contentSize != self.scrollView.contentSize { if scrollContentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize self.scrollView.contentSize = scrollContentSize
} }
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
@ -1505,7 +1605,7 @@ public final class StoryItemSetContainerComponent: Component {
self.contentContainerView.addSubview(navigationStripView) 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.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] = [] var items: [StoryActionsComponent.Item] = []
@ -1542,7 +1642,7 @@ public final class StoryItemSetContainerComponent: Component {
if self.displayReactions { if self.displayReactions {
inlineActionsAlpha = 0.0 inlineActionsAlpha = 0.0
} }
if component.hideUI { if component.hideUI || self.displayViewList {
inlineActionsAlpha = 0.0 inlineActionsAlpha = 0.0
} }

View File

@ -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)
}
}

View File

@ -19,6 +19,7 @@ public final class StoryContentContextImpl: StoryContentContext {
private let peerId: EnginePeer.Id private let peerId: EnginePeer.Id
private(set) var sliceValue: StoryContentContextState.FocusedSlice? private(set) var sliceValue: StoryContentContextState.FocusedSlice?
fileprivate var nextItems: [EngineStoryItem] = []
let updated = Promise<Void>() let updated = Promise<Void>()
@ -154,6 +155,25 @@ public final class StoryContentContextImpl: StoryContentContext {
isPublic: item.isPublic 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( self.sliceValue = StoryContentContextState.FocusedSlice(
peer: peer, peer: peer,
item: StoryContentItem( item: StoryContentItem(
@ -314,6 +334,8 @@ public final class StoryContentContextImpl: StoryContentContext {
private var requestedStoryKeys = Set<StoryKey>() private var requestedStoryKeys = Set<StoryKey>()
private var requestStoryDisposables = DisposableSet() private var requestStoryDisposables = DisposableSet()
private var preloadStoryResourceDisposables: [MediaResourceId: Disposable] = [:]
public init( public init(
context: AccountContext, context: AccountContext,
focusedPeerId: EnginePeer.Id? focusedPeerId: EnginePeer.Id?
@ -336,6 +358,9 @@ public final class StoryContentContextImpl: StoryContentContext {
deinit { deinit {
self.storySubscriptionsDisposable?.dispose() self.storySubscriptionsDisposable?.dispose()
self.requestStoryDisposables.dispose() self.requestStoryDisposables.dispose()
for (_, disposable) in self.preloadStoryResourceDisposables {
disposable.dispose()
}
} }
private func updatePeerContexts() { private func updatePeerContexts() {
@ -486,6 +511,82 @@ public final class StoryContentContextImpl: StoryContentContext {
self.statePromise.set(.single(stateValue)) self.statePromise.set(.single(stateValue))
self.updatedPromise.set(.single(Void())) 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() { public func resetSideStates() {

View File

@ -169,13 +169,19 @@ final class StoryItemContentComponent: Component {
self.videoNode = videoNode self.videoNode = videoNode
self.addSubnode(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 videoNode.ownsContentNodeUpdated = { [weak self] value in
guard let self else { guard let self else {
return return
} }
if value { if value {
self.videoNode?.seek(0.0) self.videoNode?.seek(0.0)
self.videoNode?.playOnceWithSound(playAndRecord: false) self.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
} }
} }
videoNode.canAttachContent = true videoNode.canAttachContent = true
@ -404,9 +410,7 @@ final class StoryItemContentComponent: Component {
wasSynchronous = false wasSynchronous = false
} }
#if DEBUG
self.performActionAfterImageContentLoaded(update: false) self.performActionAfterImageContentLoaded(update: false)
#endif
self.fetchDisposable?.dispose() self.fetchDisposable?.dispose()
self.fetchDisposable = nil self.fetchDisposable = nil

View File

@ -12,17 +12,20 @@ import TelegramCore
public final class StoryFooterPanelComponent: Component { public final class StoryFooterPanelComponent: Component {
public let context: AccountContext public let context: AccountContext
public let storyItem: EngineStoryItem? public let storyItem: EngineStoryItem?
public let expandViewStats: () -> Void
public let deleteAction: () -> Void public let deleteAction: () -> Void
public let moreAction: (UIView, ContextGesture?) -> Void public let moreAction: (UIView, ContextGesture?) -> Void
public init( public init(
context: AccountContext, context: AccountContext,
storyItem: EngineStoryItem?, storyItem: EngineStoryItem?,
expandViewStats: @escaping () -> Void,
deleteAction: @escaping () -> Void, deleteAction: @escaping () -> Void,
moreAction: @escaping (UIView, ContextGesture?) -> Void moreAction: @escaping (UIView, ContextGesture?) -> Void
) { ) {
self.context = context self.context = context
self.storyItem = storyItem self.storyItem = storyItem
self.expandViewStats = expandViewStats
self.deleteAction = deleteAction self.deleteAction = deleteAction
self.moreAction = moreAction self.moreAction = moreAction
} }
@ -38,6 +41,7 @@ public final class StoryFooterPanelComponent: Component {
} }
public final class View: UIView { public final class View: UIView {
private let viewStatsButton: HighlightableButton
private let viewStatsText = ComponentView<Empty>() private let viewStatsText = ComponentView<Empty>()
private let deleteButton = ComponentView<Empty>() private let deleteButton = ComponentView<Empty>()
private var moreButton: MoreHeaderButton? private var moreButton: MoreHeaderButton?
@ -49,18 +53,31 @@ public final class StoryFooterPanelComponent: Component {
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
override init(frame: CGRect) { override init(frame: CGRect) {
self.viewStatsButton = HighlightableButton()
self.avatarsContext = AnimatedAvatarSetContext() self.avatarsContext = AnimatedAvatarSetContext()
self.avatarsNode = AnimatedAvatarSetNode() self.avatarsNode = AnimatedAvatarSetNode()
super.init(frame: frame) 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) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") 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 { func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.state = state self.state = state
@ -85,16 +102,22 @@ public final class StoryFooterPanelComponent: Component {
leftOffset = avatarsNodeFrame.maxX + avatarSpacing leftOffset = avatarsNodeFrame.maxX + avatarSpacing
} }
let viewsText: String var viewCount = 0
if let views = component.storyItem?.views, views.seenCount != 0 { if let views = component.storyItem?.views, views.seenCount != 0 {
if views.seenCount == 1 { viewCount = views.seenCount
}
let viewsText: String
if viewCount == 0 {
viewsText = "No Views"
} else if viewCount == 1 {
viewsText = "1 view" viewsText = "1 view"
} else { } else {
viewsText = "\(views.seenCount) views" viewsText = "\(viewCount) views"
}
} else {
viewsText = "No views yet"
} }
self.viewStatsButton.isEnabled = viewCount != 0
let viewStatsTextSize = self.viewStatsText.update( let viewStatsTextSize = self.viewStatsText.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)), 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 let viewStatsTextView = self.viewStatsText.view {
if viewStatsTextView.superview == nil { if viewStatsTextView.superview == nil {
viewStatsTextView.layer.anchorPoint = CGPoint() viewStatsTextView.layer.anchorPoint = CGPoint()
self.addSubview(viewStatsTextView) viewStatsTextView.isUserInteractionEnabled = false
self.viewStatsButton.addSubview(viewStatsTextView)
} }
transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin) transition.setPosition(view: viewStatsTextView, position: viewStatsTextFrame.origin)
transition.setBounds(view: viewStatsTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsTextFrame.size)) 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 var rightContentOffset: CGFloat = availableSize.width - 12.0
let deleteButtonSize = self.deleteButton.update( let deleteButtonSize = self.deleteButton.update(

View File

@ -19,6 +19,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/ContextUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -25,6 +25,7 @@ public final class StoryPeerListComponent: Component {
public let collapseFraction: CGFloat public let collapseFraction: CGFloat
public let uploadProgress: Float? public let uploadProgress: Float?
public let peerAction: (EnginePeer?) -> Void public let peerAction: (EnginePeer?) -> Void
public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public init( public init(
externalState: ExternalState, externalState: ExternalState,
@ -34,7 +35,8 @@ public final class StoryPeerListComponent: Component {
storySubscriptions: EngineStorySubscriptions?, storySubscriptions: EngineStorySubscriptions?,
collapseFraction: CGFloat, collapseFraction: CGFloat,
uploadProgress: Float?, uploadProgress: Float?,
peerAction: @escaping (EnginePeer?) -> Void peerAction: @escaping (EnginePeer?) -> Void,
contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
) { ) {
self.externalState = externalState self.externalState = externalState
self.context = context self.context = context
@ -44,6 +46,7 @@ public final class StoryPeerListComponent: Component {
self.collapseFraction = collapseFraction self.collapseFraction = collapseFraction
self.uploadProgress = uploadProgress self.uploadProgress = uploadProgress
self.peerAction = peerAction self.peerAction = peerAction
self.contextPeerAction = contextPeerAction
} }
public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool { public static func ==(lhs: StoryPeerListComponent, rhs: StoryPeerListComponent) -> Bool {
@ -319,7 +322,8 @@ public final class StoryPeerListComponent: Component {
collapsedWidth: collapsedItemWidth, collapsedWidth: collapsedItemWidth,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance, rightNeighborDistance: rightNeighborDistance,
action: component.peerAction action: component.peerAction,
contextGesture: component.contextPeerAction
)), )),
environment: {}, environment: {},
containerSize: itemLayout.itemSize containerSize: itemLayout.itemSize

View File

@ -9,6 +9,8 @@ import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import AvatarNode import AvatarNode
import ContextUI
import AsyncDisplayKit
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? { 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) 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 leftNeighborDistance: CGFloat?
public let rightNeighborDistance: CGFloat? public let rightNeighborDistance: CGFloat?
public let action: (EnginePeer) -> Void public let action: (EnginePeer) -> Void
public let contextGesture: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
public init( public init(
context: AccountContext, context: AccountContext,
@ -165,7 +168,8 @@ public final class StoryPeerListItemComponent: Component {
collapsedWidth: CGFloat, collapsedWidth: CGFloat,
leftNeighborDistance: CGFloat?, leftNeighborDistance: CGFloat?,
rightNeighborDistance: CGFloat?, rightNeighborDistance: CGFloat?,
action: @escaping (EnginePeer) -> Void action: @escaping (EnginePeer) -> Void,
contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -179,6 +183,7 @@ public final class StoryPeerListItemComponent: Component {
self.leftNeighborDistance = leftNeighborDistance self.leftNeighborDistance = leftNeighborDistance
self.rightNeighborDistance = rightNeighborDistance self.rightNeighborDistance = rightNeighborDistance
self.action = action self.action = action
self.contextGesture = contextGesture
} }
public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool { public static func ==(lhs: StoryPeerListItemComponent, rhs: StoryPeerListItemComponent) -> Bool {
@ -218,7 +223,13 @@ public final class StoryPeerListItemComponent: Component {
return true 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 let avatarContainer: UIView
private var avatarNode: AvatarNode? private var avatarNode: AvatarNode?
private var avatarAddBadgeView: UIImageView? private var avatarAddBadgeView: UIImageView?
@ -233,6 +244,13 @@ public final class StoryPeerListItemComponent: Component {
private weak var componentState: EmptyComponentState? private weak var componentState: EmptyComponentState?
public override init(frame: CGRect) { 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 = UIView()
self.avatarContainer.isUserInteractionEnabled = false self.avatarContainer.isUserInteractionEnabled = false
@ -248,9 +266,16 @@ public final class StoryPeerListItemComponent: Component {
super.init(frame: frame) 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.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
self.indicatorColorLayer.mask = self.indicatorMaskLayer self.indicatorColorLayer.mask = self.indicatorMaskLayer
@ -262,7 +287,7 @@ public final class StoryPeerListItemComponent: Component {
self.indicatorShapeLayer.lineWidth = 2.0 self.indicatorShapeLayer.lineWidth = 2.0
self.indicatorShapeLayer.lineCap = .round self.indicatorShapeLayer.lineCap = .round
self.highligthedChanged = { [weak self] highlighted in self.button.highligthedChanged = { [weak self] highlighted in
guard let self else { guard let self else {
return return
} }
@ -274,7 +299,36 @@ public final class StoryPeerListItemComponent: Component {
self.layer.animateAlpha(from: previousAlpha, to: self.alpha, duration: 0.25) 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) { 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 { 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 hadUnseen = self.component?.hasUnseen
let hadProgress = self.component?.progress != nil let hadProgress = self.component?.progress != nil
let themeUpdated = self.component?.theme !== component.theme let themeUpdated = self.component?.theme !== component.theme
let previousComponent = self.component let previousComponent = self.component
self.containerNode.isGestureEnabled = component.peer.id != component.context.account.peerId
self.component = component self.component = component
self.componentState = state self.componentState = state
@ -464,7 +531,7 @@ public final class StoryPeerListItemComponent: Component {
if titleView.superview == nil { if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint() titleView.layer.anchorPoint = CGPoint()
titleView.isUserInteractionEnabled = false titleView.isUserInteractionEnabled = false
self.addSubview(titleView) self.button.addSubview(titleView)
} }
titleTransition.setPosition(view: titleView, position: titleFrame.origin) titleTransition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)

View File

@ -260,7 +260,7 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
if let mediaEditorValues { if let mediaEditorValues {
let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, frameRate: 30.0) let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, frameRate: 30.0)
let videoExport = MediaEditorVideoExport(account: account, subject: .video(avAsset), configuration: configuration, outputPath: tempFile.path) 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 let statusDisposable = videoExport.status.start(next: { status in
switch status { switch status {
@ -293,6 +293,8 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
EngineTempBox.shared.dispose(tempFile) EngineTempBox.shared.dispose(tempFile)
case .failed: case .failed:
subscriber.putError(.generic) subscriber.putError(.generic)
case let .progress(progress):
subscriber.putNext(.progressUpdated(progress))
default: default:
break break
} }
@ -414,7 +416,7 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo
} }
let videoExport = MediaEditorVideoExport(account: account, subject: subject, configuration: configuration, outputPath: tempFile.path) let videoExport = MediaEditorVideoExport(account: account, subject: subject, configuration: configuration, outputPath: tempFile.path)
videoExport.startExport() videoExport.start()
let statusDisposable = videoExport.status.start(next: { status in let statusDisposable = videoExport.status.start(next: { status in
switch status { switch status {
@ -447,6 +449,8 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo
EngineTempBox.shared.dispose(tempFile) EngineTempBox.shared.dispose(tempFile)
case .failed: case .failed:
subscriber.putError(.generic) subscriber.putError(.generic)
case let .progress(progress):
subscriber.putNext(.progressUpdated(progress))
default: default:
break break
} }

View File

@ -292,8 +292,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return nil return nil
case let .image(image): case let .image(image):
return .image(image, PixelDimensions(image.size)) return .image(image, PixelDimensions(image.size))
case let .video(path, dimensions): case let .video(path, transitionImage, dimensions):
return .video(path, dimensions) return .video(path, transitionImage, dimensions)
case let .asset(asset): case let .asset(asset):
return .asset(asset) return .asset(asset)
case let .draft(draft): case let .draft(draft):
@ -342,7 +342,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
} }
if let chatListController = self.chatListController as? ChatListControllerImpl { if let chatListController = self.chatListController as? ChatListControllerImpl {
chatListController.scrollToTop?() chatListController.scrollToStories()
switch mediaResult { switch mediaResult {
case let .image(image, dimensions, caption): case let .image(image, dimensions, caption):
if let imageData = compressImageToJPEG(image, quality: 0.6) { if let imageData = compressImageToJPEG(image, quality: 0.6) {