[WIP] Stories

This commit is contained in:
Ali 2023-05-19 18:10:12 +04:00
parent f72f2e3c60
commit 862cb0b366
42 changed files with 3617 additions and 925 deletions

View File

@ -1393,12 +1393,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else { guard let self else {
return return
} }
let storyContainerScreen = StoryContainerScreen( let storyContainerScreen = StoryContainerScreen(
context: self.context, context: self.context,
initialFocusedId: AnyHashable(peerId), initialFocusedId: AnyHashable(peerId),
initialContent: initialContent, initialContent: initialContent,
transitionIn: nil, transitionIn: nil,
transitionOut: { _ in transitionOut: { _, _ in
return nil return nil
} }
) )
@ -2481,28 +2482,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
initialFocusedId: initialFocusedId, initialFocusedId: initialFocusedId,
initialContent: initialContent, initialContent: initialContent,
transitionIn: transitionIn, transitionIn: transitionIn,
transitionOut: { [weak self] peerId in transitionOut: { [weak self] peerId, _ in
guard let self else { guard let self else {
return nil return nil
} }
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) { if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
//let localRect = transitionView.convert(transitionView.bounds, to: self.view)
/*Queue.mainQueue().after(0.2 * UIView.animationDurationFactor, { [weak self] in
HapticFeedback().impact()
self?.animateRipple(centerLocation: localRect.center)
})*/
return StoryContainerScreen.TransitionOut( return StoryContainerScreen.TransitionOut(
destinationView: transitionView, destinationView: transitionView,
destinationRect: transitionView.bounds, destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5, destinationCornerRadius: transitionView.bounds.height * 0.5,
completed: { [weak self] in completed: {}
let _ = self
//self?.animateRipple(centerLocation: localRect.center)
}
) )
} }
} }

View File

@ -432,7 +432,7 @@ public struct Transition {
self.setTransform(layer: view.layer, transform: transform, completion: completion) self.setTransform(layer: view.layer, transform: transform, completion: completion)
} }
public func setTransformAsKeyframes(view: UIView, transform: (CGFloat) -> CATransform3D, completion: ((Bool) -> Void)? = nil) { public func setTransformAsKeyframes(view: UIView, transform: (CGFloat, Bool) -> CATransform3D, completion: ((Bool) -> Void)? = nil) {
self.setTransformAsKeyframes(layer: view.layer, transform: transform, completion: completion) self.setTransformAsKeyframes(layer: view.layer, transform: transform, completion: completion)
} }
@ -477,8 +477,8 @@ public struct Transition {
} }
} }
public func setTransformAsKeyframes(layer: CALayer, transform: (CGFloat) -> CATransform3D, completion: ((Bool) -> Void)? = nil) { public func setTransformAsKeyframes(layer: CALayer, transform: (CGFloat, Bool) -> CATransform3D, completion: ((Bool) -> Void)? = nil) {
let finalTransform = transform(1.0) let finalTransform = transform(1.0, true)
let t = layer.presentation()?.transform ?? layer.transform let t = layer.presentation()?.transform ?? layer.transform
if CATransform3DEqualToTransform(t, finalTransform) { if CATransform3DEqualToTransform(t, finalTransform) {
@ -495,7 +495,7 @@ public struct Transition {
switch self.animation { switch self.animation {
case .none: case .none:
layer.transform = transform(1.0) layer.transform = transform(1.0, true)
completion?(true) completion?(true)
case let .curve(duration, curve): case let .curve(duration, curve):
let framesPerSecond: CGFloat let framesPerSecond: CGFloat
@ -507,7 +507,7 @@ public struct Transition {
let numValues = Int(framesPerSecond * duration) let numValues = Int(framesPerSecond * duration)
if numValues == 0 { if numValues == 0 {
layer.transform = transform(1.0) layer.transform = transform(1.0, true)
completion?(true) completion?(true)
return return
} }
@ -516,10 +516,10 @@ public struct Transition {
for i in 0 ... numValues { for i in 0 ... numValues {
let t = curve.solve(at: CGFloat(i) / CGFloat(numValues)) let t = curve.solve(at: CGFloat(i) / CGFloat(numValues))
values.append(NSValue(caTransform3D: transform(t))) values.append(NSValue(caTransform3D: transform(t, false)))
} }
layer.transform = transform(1.0) layer.transform = transform(1.0, true)
layer.animateKeyframes( layer.animateKeyframes(
values: values, values: values,
duration: duration, duration: duration,

View File

@ -111,6 +111,10 @@ public final class Button: Component {
public final class View: UIButton, ComponentTaggedView { public final class View: UIButton, ComponentTaggedView {
private let contentView: ComponentHostView<Empty> private let contentView: ComponentHostView<Empty>
public var content: UIView? {
return self.contentView.componentView
}
private var component: Button? private var component: Button?
private var currentIsHighlighted: Bool = false { private var currentIsHighlighted: Bool = false {
didSet { didSet {

View File

@ -76,8 +76,12 @@ private func storeImage(context: DrawingContext, mediaBox: MediaBox, resourceId:
switch imageType { switch imageType {
case .blurredThumbnail: case .blurredThumbnail:
representationId = "blurred32" representationId = "blurred32"
case let .square(width): case let .square(width, aspectRatio):
representationId = "shm\(width)" if aspectRatio == 1.0 {
representationId = "shm\(width)"
} else {
representationId = "shm\(width)-\(aspectRatio)"
}
} }
let path = mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general) let path = mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general)
@ -229,7 +233,7 @@ public final class DirectMediaImageCache {
fileprivate enum ImageType { fileprivate enum ImageType {
case blurredThumbnail case blurredThumbnail
case square(width: Int) case square(width: Int, aspectRatio: CGFloat)
} }
private let account: Account private let account: Account
@ -243,13 +247,17 @@ public final class DirectMediaImageCache {
switch imageType { switch imageType {
case .blurredThumbnail: case .blurredThumbnail:
representationId = "blurred32" representationId = "blurred32"
case let .square(width): case let .square(width, aspectRatio):
representationId = "shm\(width)" if aspectRatio == 1.0 {
representationId = "shm\(width)"
} else {
representationId = "shm\(width)-\(aspectRatio)"
}
} }
return self.account.postbox.mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general) return self.account.postbox.mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general)
} }
private func getLoadSignal(width: Int, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, resourceSizeLimit: Int64) -> Signal<UIImage?, NoError>? { private func getLoadSignal(width: Int, aspectRatio: CGFloat, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, resourceSizeLimit: Int64) -> Signal<UIImage?, NoError>? {
return Signal { subscriber in return Signal { subscriber in
let fetch = fetchedMediaResource( let fetch = fetchedMediaResource(
mediaBox: self.account.postbox.mediaBox, mediaBox: self.account.postbox.mediaBox,
@ -282,7 +290,7 @@ public final class DirectMediaImageCache {
let data = dataSignal.start(next: { data in let data = dataSignal.start(next: { data in
if let data = data, let image = UIImage(data: data) { if let data = data, let image = UIImage(data: data) {
let scaledSize = CGSize(width: CGFloat(width), height: CGFloat(width)) let scaledSize = CGSize(width: CGFloat(width), height: floor(CGFloat(width) / aspectRatio))
guard let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true) else { guard let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true) else {
subscriber.putNext(nil) subscriber.putNext(nil)
subscriber.putCompletion() subscriber.putCompletion()
@ -294,7 +302,7 @@ public final class DirectMediaImageCache {
context.draw(image.cgImage!, in: imageRect) context.draw(image.cgImage!, in: imageRect)
} }
if let scaledImage = storeImage(context: scaledContext, mediaBox: self.account.postbox.mediaBox, resourceId: resource.resource.id, imageType: .square(width: width)) { if let scaledImage = storeImage(context: scaledContext, mediaBox: self.account.postbox.mediaBox, resourceId: resource.resource.id, imageType: .square(width: width, aspectRatio: aspectRatio)) {
subscriber.putNext(scaledImage) subscriber.putNext(scaledImage)
subscriber.putCompletion() subscriber.putCompletion()
} }
@ -342,8 +350,16 @@ public final class DirectMediaImageCache {
private func getResource(message: Message, file: TelegramMediaFile, width: Int) -> (resource: MediaResourceReference, size: Int64)? { private func getResource(message: Message, file: TelegramMediaFile, width: Int) -> (resource: MediaResourceReference, size: Int64)? {
return self.getProgressiveSize(mediaReference: MediaReference.message(message: MessageReference(message), media: file).abstract, width: width, representations: file.previewRepresentations) return self.getProgressiveSize(mediaReference: MediaReference.message(message: MessageReference(message), media: file).abstract, width: width, representations: file.previewRepresentations)
} }
private func getResource(peer: PeerReference, story: StoryListContext.Item, image: TelegramMediaImage, width: Int) -> (resource: MediaResourceReference, size: Int64)? {
return self.getProgressiveSize(mediaReference: MediaReference.story(peer: peer, id: story.id, media: image).abstract, width: width, representations: image.representations)
}
private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool) -> GetMediaResult? { private func getResource(peer: PeerReference, story: StoryListContext.Item, file: TelegramMediaFile, width: Int) -> (resource: MediaResourceReference, size: Int64)? {
return self.getProgressiveSize(mediaReference: MediaReference.story(peer: peer, id: story.id, media: file).abstract, width: width, representations: file.previewRepresentations)
}
private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, aspectRatio: CGFloat, possibleWidths: [Int], includeBlurred: Bool) -> GetMediaResult? {
var immediateThumbnailData: Data? var immediateThumbnailData: Data?
var resource: (resource: MediaResourceReference, size: Int64)? var resource: (resource: MediaResourceReference, size: Int64)?
if let image = media as? TelegramMediaImage { if let image = media as? TelegramMediaImage {
@ -367,11 +383,11 @@ public final class DirectMediaImageCache {
var resultImage: UIImage? var resultImage: UIImage?
for otherWidth in possibleWidths.reversed() { for otherWidth in possibleWidths.reversed() {
if otherWidth == width { if otherWidth == width {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth, aspectRatio: aspectRatio)))), let image = loadImage(data: data) {
return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil) return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil)
} }
} else { } else {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth, aspectRatio: aspectRatio)))), let image = loadImage(data: data) {
resultImage = image resultImage = image
} }
} }
@ -387,12 +403,12 @@ public final class DirectMediaImageCache {
} }
} }
return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size)) return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, aspectRatio: aspectRatio, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size))
} }
public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? { public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? {
if synchronous { if synchronous {
return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred) return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, aspectRatio: 1.0, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
} else { } else {
var immediateThumbnailData: Data? var immediateThumbnailData: Data?
if let image = media as? TelegramMediaImage { if let image = media as? TelegramMediaImage {
@ -405,7 +421,93 @@ public final class DirectMediaImageCache {
blurredImage = blurredImageValue blurredImage = blurredImageValue
} }
return GetMediaResult(image: nil, blurredImage: blurredImage, loadSignal: Signal { subscriber in return GetMediaResult(image: nil, blurredImage: blurredImage, loadSignal: Signal { subscriber in
let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred) let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, aspectRatio: 1.0, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
guard let result = result else {
subscriber.putNext(nil)
subscriber.putCompletion()
return EmptyDisposable
}
if let image = result.image {
subscriber.putNext(image)
}
if let signal = result.loadSignal {
return signal.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion)
} else {
subscriber.putCompletion()
return EmptyDisposable
}
}
|> runOn(.concurrentDefaultQueue()))
}
}
private func getImageSynchronous(peer: PeerReference, story: StoryListContext.Item, userLocation: MediaResourceUserLocation, media: Media, width: Int, aspectRatio: CGFloat, possibleWidths: [Int], includeBlurred: Bool) -> GetMediaResult? {
var immediateThumbnailData: Data?
var resource: (resource: MediaResourceReference, size: Int64)?
if let image = media as? TelegramMediaImage {
immediateThumbnailData = image.immediateThumbnailData
resource = self.getResource(peer: peer, story: story, image: image, width: width)
} else if let file = media as? TelegramMediaFile {
immediateThumbnailData = file.immediateThumbnailData
resource = self.getResource(peer: peer, story: story, file: file, width: width)
}
guard let resource = resource else {
return nil
}
var blurredImage: UIImage?
if includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) {
blurredImage = blurredImageValue
}
var resultImage: UIImage?
for otherWidth in possibleWidths.reversed() {
if otherWidth == width {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth, aspectRatio: aspectRatio)))), let image = loadImage(data: data) {
return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil)
}
} else {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth, aspectRatio: aspectRatio)))), let image = loadImage(data: data) {
resultImage = image
}
}
}
if resultImage == nil {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) {
resultImage = image
} else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) {
if let blurredImageValue = generateBlurredThumbnail(image: image) {
resultImage = blurredImageValue
}
}
}
return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, aspectRatio: aspectRatio, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size))
}
public func getImage(peer: PeerReference, story: StoryListContext.Item, media: Media, width: Int, aspectRatio: CGFloat, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? {
if synchronous {
return self.getImageSynchronous(peer: peer, story: story, userLocation: .peer(peer.id), media: media, width: width, aspectRatio: aspectRatio, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
} else {
var immediateThumbnailData: Data?
if let image = media as? TelegramMediaImage {
immediateThumbnailData = image.immediateThumbnailData
} else if let file = media as? TelegramMediaFile {
immediateThumbnailData = file.immediateThumbnailData
}
var blurredImage: UIImage?
if includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) {
blurredImage = blurredImageValue
}
return GetMediaResult(image: nil, blurredImage: blurredImage, loadSignal: Signal { subscriber in
let result = self.getImageSynchronous(peer: peer, story: story, userLocation: .peer(peer.id), media: media, width: width, aspectRatio: aspectRatio, possibleWidths: possibleWidths, includeBlurred: includeBlurred)
guard let result = result else { guard let result = result else {
subscriber.putNext(nil) subscriber.putNext(nil)
subscriber.putCompletion() subscriber.putCompletion()

View File

@ -249,6 +249,7 @@ public extension CALayer {
animation.speed = speed animation.speed = speed
animation.duration = duration animation.duration = duration
animation.isAdditive = additive animation.isAdditive = additive
animation.calculationMode = .linear
if let mediaTimingFunction = mediaTimingFunction { if let mediaTimingFunction = mediaTimingFunction {
animation.timingFunction = mediaTimingFunction animation.timingFunction = mediaTimingFunction
} else { } else {

View File

@ -3,6 +3,15 @@ import AVFoundation
import UIKit import UIKit
import MozjpegBinding import MozjpegBinding
public func scaleImageToPixelSize(image: UIImage, size: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, true, 1.0)
image.draw(in: CGRect(origin: CGPoint(), size: size), blendMode: .copy, alpha: 1.0)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
public func extractImageExtraScans(_ data: Data) -> [Int] { public func extractImageExtraScans(_ data: Data) -> [Int] {
return extractJPEGDataScans(data).map { item in return extractJPEGDataScans(data).map { item in
return item.intValue return item.intValue

View File

@ -405,7 +405,11 @@ public final class SparseItemGrid: ASDisplayNode {
let itemsPerRow = CGFloat(zoomLevel.rawValue) let itemsPerRow = CGFloat(zoomLevel.rawValue)
self.itemsPerRow = Int(itemsPerRow) self.itemsPerRow = Int(itemsPerRow)
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow) let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
self.itemSize = CGSize(width: itemSize, height: itemSize) if let fixedItemAspect = containerLayout.fixedItemAspect {
self.itemSize = CGSize(width: itemSize, height: floor(itemSize / fixedItemAspect))
} else {
self.itemSize = CGSize(width: itemSize, height: itemSize)
}
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1) self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
} }
@ -1308,6 +1312,7 @@ public final class SparseItemGrid: ASDisplayNode {
var scrollIndicatorInsets: UIEdgeInsets var scrollIndicatorInsets: UIEdgeInsets
var lockScrollingAtTop: Bool var lockScrollingAtTop: Bool
var fixedItemHeight: CGFloat? var fixedItemHeight: CGFloat?
var fixedItemAspect: CGFloat?
} }
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
@ -1565,9 +1570,9 @@ public final class SparseItemGrid: ASDisplayNode {
} }
} }
public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous) { public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous) {
self.theme = theme self.theme = theme
let containerLayout = ContainerLayout(size: size, insets: insets, useSideInsets: useSideInsets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight) let containerLayout = ContainerLayout(size: size, insets: insets, useSideInsets: useSideInsets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect)
self.containerLayout = containerLayout self.containerLayout = containerLayout
self.items = items self.items = items
self.scrollingArea.isHidden = lockScrollingAtTop self.scrollingArea.isHidden = lockScrollingAtTop

View File

@ -107,7 +107,7 @@ class Download: NSObject, MTRequestMessageServiceDelegate {
let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
if asBigPart { if asBigPart {
let totalParts: Int32 let totalParts: Int32
if let bigTotalParts = bigTotalParts { if let bigTotalParts = bigTotalParts, bigTotalParts > 0 && bigTotalParts < Int32.max {
totalParts = Int32(bigTotalParts) totalParts = Int32(bigTotalParts)
} else { } else {
totalParts = -1 totalParts = -1

View File

@ -172,17 +172,19 @@ public final class StoryListContext {
self.stateValue = State(itemSets: [], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil)) self.stateValue = State(itemSets: [], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil))
self.state.set(.single(self.stateValue)) self.state.set(.single(self.stateValue))
let _ = (account.postbox.transaction { transaction -> Peer? in if case .all = scope {
return transaction.getPeer(account.peerId) let _ = (account.postbox.transaction { transaction -> Peer? in
} return transaction.getPeer(account.peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
} }
self.stateValue = State(itemSets: [ |> deliverOnMainQueue).start(next: { [weak self] peer in
PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), maxReadId: 0, items: [], totalCount: 0) guard let self, let peer else {
], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil)) return
}) }
self.stateValue = State(itemSets: [
PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), maxReadId: 0, items: [], totalCount: 0)
], uploadProgress: nil, loadMoreToken: LoadMoreToken(value: nil))
})
}
self.updatesDisposable = (account.stateManager.storyUpdates self.updatesDisposable = (account.stateManager.storyUpdates
|> deliverOn(queue)).start(next: { [weak self] updates in |> deliverOn(queue)).start(next: { [weak self] updates in
@ -253,7 +255,12 @@ public final class StoryListContext {
items.sort(by: { lhsItem, rhsItem in items.sort(by: { lhsItem, rhsItem in
if lhsItem.timestamp != rhsItem.timestamp { if lhsItem.timestamp != rhsItem.timestamp {
return lhsItem.timestamp < rhsItem.timestamp switch scope {
case .all:
return lhsItem.timestamp > rhsItem.timestamp
case .peer:
return lhsItem.timestamp < rhsItem.timestamp
}
} }
return lhsItem.id < rhsItem.id return lhsItem.id < rhsItem.id
}) })
@ -267,13 +274,23 @@ public final class StoryListContext {
} }
} }
if !found, let peer = peers[peerId] { if !found, let peer = peers[peerId] {
itemSets.insert(PeerItemSet( let matchesScope: Bool
peerId: peerId, if case .all = scope {
peer: EnginePeer(peer), matchesScope = true
maxReadId: 0, } else if case .peer(peerId) = scope {
items: [item], matchesScope = true
totalCount: 1 } else {
), at: 0) matchesScope = false
}
if matchesScope {
itemSets.insert(PeerItemSet(
peerId: peerId,
peer: EnginePeer(peer),
maxReadId: 0,
items: [item],
totalCount: 1
), at: 0)
}
} }
case let .read(peerId, maxId): case let .read(peerId, maxId):
for i in 0 ..< itemSets.count { for i in 0 ..< itemSets.count {
@ -301,7 +318,12 @@ public final class StoryListContext {
} }
if lhsItem.timestamp != rhsItem.timestamp { if lhsItem.timestamp != rhsItem.timestamp {
return lhsItem.timestamp > rhsItem.timestamp switch scope {
case .all:
return lhsItem.timestamp > rhsItem.timestamp
case .peer:
return lhsItem.timestamp < rhsItem.timestamp
}
} }
return lhsItem.id > rhsItem.id return lhsItem.id > rhsItem.id
}) })
@ -342,7 +364,7 @@ public final class StoryListContext {
guard let inputPeer = inputPeer else { guard let inputPeer = inputPeer else {
return .single(nil) return .single(nil)
} }
return account.network.request(Api.functions.stories.getUserStories(flags: 0, userId: inputPeer, offsetId: 0, limit: 100)) return account.network.request(Api.functions.stories.getUserStories(flags: 0, userId: inputPeer, offsetId: 0, limit: 30))
|> map(Optional.init) |> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in |> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
return .single(nil) return .single(nil)
@ -433,139 +455,220 @@ public final class StoryListContext {
self.isLoadingMore = true self.isLoadingMore = true
let account = self.account let account = self.account
let scope = self.scope
self.pollDisposable?.dispose() self.pollDisposable?.dispose()
self.pollDisposable = nil self.pollDisposable = nil
self.loadMoreDisposable.set((account.network.request(Api.functions.stories.getAllStories(offset: loadMoreToken)) switch scope {
|> map(Optional.init) case .all:
|> `catch` { _ -> Signal<Api.stories.AllStories?, NoError> in self.loadMoreDisposable.set((account.network.request(Api.functions.stories.getAllStories(offset: loadMoreToken))
return .single(nil) |> map(Optional.init)
} |> `catch` { _ -> Signal<Api.stories.AllStories?, NoError> in
|> mapToSignal { result -> Signal<([PeerItemSet], LoadMoreToken?), NoError> in return .single(nil)
guard let result else {
return .single(([], nil))
} }
return account.postbox.transaction { transaction -> ([PeerItemSet], LoadMoreToken?) in |> mapToSignal { result -> Signal<([PeerItemSet], LoadMoreToken?), NoError> in
switch result { guard let result else {
case let .allStories(_, userStorySets, nextOffset, users): return .single(([], nil))
var parsedItemSets: [PeerItemSet] = [] }
return account.postbox.transaction { transaction -> ([PeerItemSet], LoadMoreToken?) in
var peers: [Peer] = [] switch result {
var peerPresences: [PeerId: Api.User] = [:] case let .allStories(_, userStorySets, nextOffset, users):
var parsedItemSets: [PeerItemSet] = []
for user in users {
let telegramUser = TelegramUser(user: user) var peers: [Peer] = []
peers.append(telegramUser) var peerPresences: [PeerId: Api.User] = [:]
peerPresences[telegramUser.id] = user
} for user in users {
let telegramUser = TelegramUser(user: user)
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in peers.append(telegramUser)
return updated peerPresences[telegramUser.id] = user
})
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
for userStories in userStorySets {
let apiUserId: Int64
let apiStories: [Api.StoryItem]
var apiTotalCount: Int32?
var apiMaxReadId: Int32 = 0
switch userStories {
case let .userStories(_, userId, maxReadId, stories, missingCount):
apiUserId = userId
apiStories = stories
apiTotalCount = (missingCount ?? 0) + Int32(stories.count)
apiMaxReadId = maxReadId ?? 0
} }
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(apiUserId)) updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
for apiStory in apiStories { return updated
if let item = _internal_parseApiStoryItem(transaction: transaction, peerId: peerId, apiStory: apiStory) { })
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId { updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
parsedItemSets[parsedItemSets.count - 1].items.append(item)
} else { for userStories in userStorySets {
parsedItemSets.append(StoryListContext.PeerItemSet( let apiUserId: Int64
peerId: peerId, let apiStories: [Api.StoryItem]
peer: transaction.getPeer(peerId).flatMap(EnginePeer.init), var apiTotalCount: Int32?
maxReadId: apiMaxReadId, var apiMaxReadId: Int32 = 0
items: [item], switch userStories {
totalCount: apiTotalCount.flatMap(Int.init) case let .userStories(_, userId, maxReadId, stories, missingCount):
)) apiUserId = userId
apiStories = stories
apiTotalCount = (missingCount ?? 0) + Int32(stories.count)
apiMaxReadId = maxReadId ?? 0
}
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(apiUserId))
for apiStory in apiStories {
if let item = _internal_parseApiStoryItem(transaction: transaction, peerId: peerId, apiStory: apiStory) {
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId {
parsedItemSets[parsedItemSets.count - 1].items.append(item)
} else {
parsedItemSets.append(StoryListContext.PeerItemSet(
peerId: peerId,
peer: transaction.getPeer(peerId).flatMap(EnginePeer.init),
maxReadId: apiMaxReadId,
items: [item],
totalCount: apiTotalCount.flatMap(Int.init)
))
}
} }
} }
} }
}
if !parsedItemSets.contains(where: { $0.peerId == account.peerId }) {
if !parsedItemSets.contains(where: { $0.peerId == account.peerId }) { if let peer = transaction.getPeer(account.peerId) {
if let peer = transaction.getPeer(account.peerId) { parsedItemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), maxReadId: 0, items: [], totalCount: 0), at: 0)
parsedItemSets.insert(PeerItemSet(peerId: peer.id, peer: EnginePeer(peer), maxReadId: 0, items: [], totalCount: 0), at: 0) }
} }
return (parsedItemSets, nextOffset.flatMap { LoadMoreToken(value: $0) })
} }
return (parsedItemSets, nextOffset.flatMap { LoadMoreToken(value: $0) })
} }
} }
} |> deliverOn(self.queue)).start(next: { [weak self] result in
|> deliverOn(self.queue)).start(next: { [weak self] result in guard let `self` = self else {
guard let `self` = self else { return
return
}
self.isLoadingMore = false
var itemSets = self.stateValue.itemSets
for itemSet in result.0 {
if let index = itemSets.firstIndex(where: { $0.peerId == itemSet.peerId }) {
let currentItemSet = itemSets[index]
var items = currentItemSet.items
for item in itemSet.items {
if !items.contains(where: { $0.id == item.id }) {
items.append(item)
}
}
items.sort(by: { lhsItem, rhsItem in
if lhsItem.timestamp != rhsItem.timestamp {
return lhsItem.timestamp < rhsItem.timestamp
}
return lhsItem.id < rhsItem.id
})
itemSets[index] = PeerItemSet(
peerId: itemSet.peerId,
peer: itemSet.peer,
maxReadId: itemSet.maxReadId,
items: items,
totalCount: items.count
)
} else {
itemSet.items.sort(by: { lhsItem, rhsItem in
if lhsItem.timestamp != rhsItem.timestamp {
return lhsItem.timestamp < rhsItem.timestamp
}
return lhsItem.id < rhsItem.id
})
itemSets.append(itemSet)
} }
} self.isLoadingMore = false
itemSets.sort(by: { lhs, rhs in var itemSets = self.stateValue.itemSets
guard let lhsItem = lhs.items.first, let rhsItem = rhs.items.first else { for itemSet in result.0 {
if lhs.items.first != nil { if let index = itemSets.firstIndex(where: { $0.peerId == itemSet.peerId }) {
return false let currentItemSet = itemSets[index]
var items = currentItemSet.items
for item in itemSet.items {
if !items.contains(where: { $0.id == item.id }) {
items.append(item)
}
}
items.sort(by: { lhsItem, rhsItem in
if lhsItem.timestamp != rhsItem.timestamp {
switch scope {
case .all:
return lhsItem.timestamp > rhsItem.timestamp
case .peer:
return lhsItem.timestamp < rhsItem.timestamp
}
}
return lhsItem.id < rhsItem.id
})
itemSets[index] = PeerItemSet(
peerId: itemSet.peerId,
peer: itemSet.peer,
maxReadId: itemSet.maxReadId,
items: items,
totalCount: items.count
)
} else { } else {
return true itemSet.items.sort(by: { lhsItem, rhsItem in
if lhsItem.timestamp != rhsItem.timestamp {
switch scope {
case .all:
return lhsItem.timestamp > rhsItem.timestamp
case .peer:
return lhsItem.timestamp < rhsItem.timestamp
}
}
return lhsItem.id < rhsItem.id
})
itemSets.append(itemSet)
} }
} }
if lhsItem.timestamp != rhsItem.timestamp { itemSets.sort(by: { lhs, rhs in
return lhsItem.timestamp > rhsItem.timestamp guard let lhsItem = lhs.items.first, let rhsItem = rhs.items.first else {
} if lhs.items.first != nil {
return lhsItem.id > rhsItem.id return false
}) } else {
return true
}
}
if lhsItem.timestamp != rhsItem.timestamp {
switch scope {
case .all:
return lhsItem.timestamp > rhsItem.timestamp
case .peer:
return lhsItem.timestamp < rhsItem.timestamp
}
}
return lhsItem.id > rhsItem.id
})
self.stateValue = State(itemSets: itemSets, uploadProgress: self.stateValue.uploadProgress, loadMoreToken: result.1)
}))
case let .peer(peerId):
let account = self.account
let queue = self.queue
self.stateValue = State(itemSets: itemSets, uploadProgress: self.stateValue.uploadProgress, loadMoreToken: result.1) self.loadMoreDisposable.set((self.account.postbox.transaction { transaction -> Api.InputUser? in
})) return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> mapToSignal { inputPeer -> Signal<PeerItemSet?, NoError> in
guard let inputPeer = inputPeer else {
return .single(nil)
}
return account.network.request(Api.functions.stories.getUserStories(flags: 0, userId: inputPeer, offsetId: 0, limit: 30))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.stories.Stories?, NoError> in
return .single(nil)
}
|> mapToSignal { stories -> Signal<PeerItemSet?, NoError> in
guard let stories = stories else {
return .single(nil)
}
return account.postbox.transaction { transaction -> PeerItemSet? in
switch stories {
case let .stories(_, apiStories, users):
var parsedItemSets: [PeerItemSet] = []
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)
for apiStory in apiStories {
if let item = _internal_parseApiStoryItem(transaction: transaction, peerId: peerId, apiStory: apiStory) {
if !parsedItemSets.isEmpty && parsedItemSets[parsedItemSets.count - 1].peerId == peerId {
parsedItemSets[parsedItemSets.count - 1].items.append(item)
parsedItemSets[parsedItemSets.count - 1].totalCount = parsedItemSets[parsedItemSets.count - 1].items.count
} else {
parsedItemSets.append(StoryListContext.PeerItemSet(peerId: peerId, peer: transaction.getPeer(peerId).flatMap(EnginePeer.init), maxReadId: 0, items: [item], totalCount: 1))
}
}
}
return parsedItemSets.first
}
}
}
}
|> deliverOn(queue)).start(next: { [weak self] itemSet in
guard let `self` = self, let itemSet = itemSet else {
return
}
self.isLoadingMore = false
self.stateValue.itemSets = [itemSet]
}))
}
} }
func delete(id: Int32) { func delete(id: Int32) {

View File

@ -370,6 +370,7 @@ swift_library(
"//submodules/TelegramUI/Components/LegacyInstantVideoController", "//submodules/TelegramUI/Components/LegacyInstantVideoController",
"//submodules/TelegramUI/Components/FullScreenEffectView", "//submodules/TelegramUI/Components/FullScreenEffectView",
"//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -7,30 +7,41 @@ import UniversalMediaPlayer
import SwiftSignalKit import SwiftSignalKit
public final class AudioWaveformComponent: Component { public final class AudioWaveformComponent: Component {
public enum Style {
case bottom
case middle
}
public let backgroundColor: UIColor public let backgroundColor: UIColor
public let foregroundColor: UIColor public let foregroundColor: UIColor
public let shimmerColor: UIColor? public let shimmerColor: UIColor?
public let style: Style
public let samples: Data public let samples: Data
public let peak: Int32 public let peak: Int32
public let status: Signal<MediaPlayerStatus, NoError> public let status: Signal<MediaPlayerStatus, NoError>
public let seek: (Double) -> Void public let seek: ((Double) -> Void)?
public let updateIsSeeking: ((Bool) -> Void)?
public init( public init(
backgroundColor: UIColor, backgroundColor: UIColor,
foregroundColor: UIColor, foregroundColor: UIColor,
shimmerColor: UIColor?, shimmerColor: UIColor?,
style: Style,
samples: Data, samples: Data,
peak: Int32, peak: Int32,
status: Signal<MediaPlayerStatus, NoError>, status: Signal<MediaPlayerStatus, NoError>,
seek: @escaping (Double) -> Void seek: ((Double) -> Void)?,
updateIsSeeking: ((Bool) -> Void)?
) { ) {
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor self.foregroundColor = foregroundColor
self.shimmerColor = shimmerColor self.shimmerColor = shimmerColor
self.style = style
self.samples = samples self.samples = samples
self.peak = peak self.peak = peak
self.status = status self.status = status
self.seek = seek self.seek = seek
self.updateIsSeeking = updateIsSeeking
} }
public static func ==(lhs: AudioWaveformComponent, rhs: AudioWaveformComponent) -> Bool { public static func ==(lhs: AudioWaveformComponent, rhs: AudioWaveformComponent) -> Bool {
@ -43,6 +54,9 @@ public final class AudioWaveformComponent: Component {
if lhs.shimmerColor != rhs.shimmerColor { if lhs.shimmerColor != rhs.shimmerColor {
return false return false
} }
if lhs.style != rhs.style {
return false
}
if lhs.samples != rhs.samples { if lhs.samples != rhs.samples {
return false return false
} }
@ -199,7 +213,6 @@ public final class AudioWaveformComponent: Component {
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self panRecognizer.delegate = self
self.addGestureRecognizer(panRecognizer) self.addGestureRecognizer(panRecognizer)
panRecognizer.isEnabled = false
self.panRecognizer = panRecognizer self.panRecognizer = panRecognizer
} }
@ -261,6 +274,7 @@ public final class AudioWaveformComponent: Component {
if let statusValue = self.playbackStatus, statusValue.duration > 0.0 { if let statusValue = self.playbackStatus, statusValue.duration > 0.0 {
self.scrubbingBeginTimestamp = statusValue.timestamp self.scrubbingBeginTimestamp = statusValue.timestamp
self.scrubbingTimestampValue = statusValue.timestamp self.scrubbingTimestampValue = statusValue.timestamp
self.component?.updateIsSeeking?(true)
self.setNeedsDisplay() self.setNeedsDisplay()
} }
} }
@ -280,7 +294,8 @@ public final class AudioWaveformComponent: Component {
}) })
if let scrubbingTimestampValue = scrubbingTimestampValue, apply { if let scrubbingTimestampValue = scrubbingTimestampValue, apply {
self.component?.seek(scrubbingTimestampValue) self.component?.seek?(scrubbingTimestampValue)
self.component?.updateIsSeeking?(false)
} }
} }
@ -523,14 +538,12 @@ public final class AudioWaveformComponent: Component {
diff = sampleWidth * 1.5 diff = sampleWidth * 1.5
let gravityMultiplierY: CGFloat let gravityMultiplierY: CGFloat
gravityMultiplierY = 1.0 switch component.style {
/*switch parameters.gravity ?? .bottom {
case .bottom: case .bottom:
return 1 gravityMultiplierY = 1.0
case .center: case .middle:
return 0.5 gravityMultiplierY = 0.5
}*/ }
context.setFillColor(component.backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor) context.setFillColor(component.backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor)
context.setBlendMode(.copy) context.setBlendMode(.copy)

View File

@ -244,6 +244,8 @@ public final class LottieComponent: Component {
var advanceFrameCount = 1 var advanceFrameCount = 1
if animationInstance.frameRate == 360 { if animationInstance.frameRate == 360 {
advanceFrameCount = 6 advanceFrameCount = 6
} else if animationInstance.frameRate == 240 {
advanceFrameCount = 4
} }
self.currentFrame += advanceFrameCount self.currentFrame += advanceFrameCount
if self.currentFrame >= Int(animationInstance.frameCount) - 1 { if self.currentFrame >= Int(animationInstance.frameCount) - 1 {

View File

@ -599,10 +599,16 @@ final class MediaEditorScreenComponent: Component {
//self.performSendMessageAction() //self.performSendMessageAction()
}, },
setMediaRecordingActive: nil, setMediaRecordingActive: nil,
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil,
attachmentAction: nil, attachmentAction: nil,
reactionAction: nil, reactionAction: nil,
audioRecorder: nil, audioRecorder: nil,
videoRecordingStatus: nil, videoRecordingStatus: nil,
isRecordingLocked: false,
recordedAudioPreview: nil,
wasRecordingDismissed: false,
displayGradient: false,//component.inputHeight != 0.0, displayGradient: false,//component.inputHeight != 0.0,
bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset
)), )),

View File

@ -19,8 +19,11 @@ swift_library(
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/AccountContext", "//submodules/AccountContext",
"//submodules/TelegramPresentationData", "//submodules/TelegramPresentationData",
"//submodules/ManagedAnimationNode",
"//submodules/SSignalKit/SwiftSignalKit", "//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Components/HierarchyTrackingLayer", "//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/AudioWaveformComponent",
"//submodules/MediaPlayer:UniversalMediaPlayer",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -0,0 +1,357 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AppBundle
import TextFieldComponent
import BundleIconComponent
import AccountContext
import TelegramPresentationData
import ChatPresentationInterfaceState
import SwiftSignalKit
import LottieComponent
import HierarchyTrackingLayer
import ManagedAnimationNode
import AudioWaveformComponent
import UniversalMediaPlayer
private final class PlayPauseIconNode: ManagedAnimationNode {
enum State: Equatable {
case play
case pause
}
private let duration: Double = 0.35
private var iconState: State = .pause
init() {
super.init(size: CGSize(width: 28.0, height: 28.0))
self.enqueueState(.play, animated: false)
}
func enqueueState(_ state: State, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}
private func textForDuration(seconds: Int32) -> String {
if seconds >= 60 * 60 {
return String(format: "%d:%02d:%02d", seconds / 3600, seconds / 60 % 60)
} else {
return String(format: "%d:%02d", seconds / 60, seconds % 60)
}
}
public final class MediaPreviewPanelComponent: Component {
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let mediaPreview: ChatRecordedMediaPreview
public let insets: UIEdgeInsets
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
mediaPreview: ChatRecordedMediaPreview,
insets: UIEdgeInsets
) {
self.context = context
self.theme = theme
self.strings = strings
self.mediaPreview = mediaPreview
self.insets = insets
}
public static func ==(lhs: MediaPreviewPanelComponent, rhs: MediaPreviewPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.mediaPreview !== rhs.mediaPreview {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
public final class View: UIView {
private var component: MediaPreviewPanelComponent?
private weak var state: EmptyComponentState?
public let vibrancyContainer: UIView
private let trackingLayer: HierarchyTrackingLayer
private let indicator = ComponentView<Empty>()
private let timerFont: UIFont
private let timerText = ComponentView<Empty>()
private var timerTextValue: String = "0:00"
private let playPauseIconButton: HighlightableButton
private let playPauseIconNode: PlayPauseIconNode
private let waveform = ComponentView<Empty>()
private let vibrancyWaveform = ComponentView<Empty>()
private var mediaPlayer: MediaPlayer?
private let mediaPlayerStatus = Promise<MediaPlayerStatus?>(nil)
private var mediaPlayerStatusDisposable: Disposable?
override init(frame: CGRect) {
self.trackingLayer = HierarchyTrackingLayer()
self.timerFont = Font.with(size: 15.0, design: .camera, traits: .monospacedNumbers)
self.vibrancyContainer = UIView()
self.playPauseIconButton = HighlightableButton()
self.playPauseIconNode = PlayPauseIconNode()
self.playPauseIconNode.isUserInteractionEnabled = false
super.init(frame: frame)
self.layer.addSublayer(self.trackingLayer)
self.playPauseIconButton.addSubview(self.playPauseIconNode.view)
self.addSubview(self.playPauseIconButton)
self.playPauseIconButton.addTarget(self, action: #selector(self.playPauseButtonPressed), for: .touchUpInside)
self.mediaPlayerStatusDisposable = (self.mediaPlayerStatus.get()
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self else {
return
}
if let status {
switch status.status {
case .playing, .buffering(_, true, _, _):
self.playPauseIconNode.enqueueState(.play, animated: true)
default:
self.playPauseIconNode.enqueueState(.pause, animated: true)
}
//self.timerTextValue = textForDuration(seconds: component.mediaPreview.duration)
} else {
self.playPauseIconNode.enqueueState(.play, animated: true)
}
})
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.mediaPlayerStatusDisposable?.dispose()
}
public func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
public func animateOut(transition: Transition, completion: @escaping () -> Void) {
let vibrancyContainer = self.vibrancyContainer
transition.setAlpha(view: vibrancyContainer, alpha: 0.0, completion: { [weak vibrancyContainer] _ in
vibrancyContainer?.removeFromSuperview()
})
transition.setAlpha(view: self, alpha: 0.0, completion: { _ in
completion()
})
}
@objc private func playPauseButtonPressed() {
guard let component = self.component else {
return
}
if let mediaPlayer = self.mediaPlayer {
mediaPlayer.togglePlayPause()
} else {
let mediaManager = component.context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(
audioSessionManager: mediaManager.audioSession,
postbox: component.context.account.postbox,
userLocation: .other,
userContentType: .audio,
resourceReference: .standalone(resource: component.mediaPreview.resource),
streamable: .none,
video: false,
preferSoftwareDecoding: false,
enableSound: true,
fetchAutomatically: true
)
mediaPlayer.actionAtEnd = .action { [weak mediaPlayer] in
mediaPlayer?.seek(timestamp: 0.0)
}
self.mediaPlayer = mediaPlayer
self.mediaPlayerStatus.set(mediaPlayer.status |> map(Optional.init))
mediaPlayer.play()
}
}
func update(component: MediaPreviewPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
if self.component == nil {
self.timerTextValue = textForDuration(seconds: component.mediaPreview.duration)
}
self.component = component
self.state = state
let timerTextSize = self.timerText.update(
transition: .immediate,
component: AnyComponent(Text(text: self.timerTextValue, font: self.timerFont, color: .white)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let timerTextView = self.timerText.view {
if timerTextView.superview == nil {
self.addSubview(timerTextView)
timerTextView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
}
let timerTextFrame = CGRect(origin: CGPoint(x: availableSize.width - component.insets.right - 8.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - timerTextSize.height) * 0.5)), size: timerTextSize)
transition.setPosition(view: timerTextView, position: CGPoint(x: timerTextFrame.minX, y: timerTextFrame.midY))
timerTextView.bounds = CGRect(origin: CGPoint(), size: timerTextFrame.size)
}
let playPauseSize = CGSize(width: 28.0, height: 28.0)
var playPauseFrame = CGRect(origin: CGPoint(x: component.insets.left + 8.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - playPauseSize.height) * 0.5)), size: playPauseSize)
let playPauseButtonFrame = playPauseFrame.insetBy(dx: -8.0, dy: -8.0)
playPauseFrame = playPauseFrame.offsetBy(dx: -playPauseButtonFrame.minX, dy: -playPauseButtonFrame.minY)
transition.setFrame(view: self.playPauseIconButton, frame: playPauseButtonFrame)
transition.setFrame(view: self.playPauseIconNode.view, frame: playPauseFrame)
let waveformFrame = CGRect(origin: CGPoint(x: component.insets.left + 47.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - 24.0) * 0.5)), size: CGSize(width: availableSize.width - component.insets.right - 47.0 - (component.insets.left + 47.0), height: 24.0))
let _ = self.waveform.update(
transition: transition,
component: AnyComponent(AudioWaveformComponent(
backgroundColor: UIColor.white.withAlphaComponent(0.1),
foregroundColor: UIColor.white.withAlphaComponent(1.0),
shimmerColor: nil,
style: .middle,
samples: component.mediaPreview.waveform.samples,
peak: component.mediaPreview.waveform.peak,
status: self.mediaPlayerStatus.get() |> map { value -> MediaPlayerStatus in
if let value {
return value
} else {
return MediaPlayerStatus(
generationTimestamp: 0.0,
duration: 0.0,
dimensions: CGSize(),
timestamp: 0.0,
baseRate: 1.0,
seekId: 0,
status: .paused,
soundEnabled: true
)
}
},
seek: { [weak self] timestamp in
guard let self, let mediaPlayer = self.mediaPlayer else {
return
}
mediaPlayer.seek(timestamp: timestamp)
},
updateIsSeeking: { [weak self] isSeeking in
guard let self, let mediaPlayer = self.mediaPlayer else {
return
}
if isSeeking {
mediaPlayer.pause()
} else {
mediaPlayer.play()
}
}
)),
environment: {},
containerSize: waveformFrame.size
)
let _ = self.vibrancyWaveform.update(
transition: transition,
component: AnyComponent(AudioWaveformComponent(
backgroundColor: .white,
foregroundColor: .white,
shimmerColor: nil,
style: .middle,
samples: component.mediaPreview.waveform.samples,
peak: component.mediaPreview.waveform.peak,
status: .complete(),
seek: nil,
updateIsSeeking: nil
)),
environment: {},
containerSize: waveformFrame.size
)
if let waveformView = self.waveform.view as? AudioWaveformComponent.View {
if waveformView.superview == nil {
waveformView.enableScrubbing = true
self.addSubview(waveformView)
}
transition.setFrame(view: waveformView, frame: waveformFrame)
}
if let vibrancyWaveformView = self.vibrancyWaveform.view {
if vibrancyWaveformView.superview == nil {
self.vibrancyContainer.addSubview(vibrancyWaveformView)
}
transition.setFrame(view: vibrancyWaveformView, frame: waveformFrame)
}
transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -13,33 +13,60 @@ import LottieComponent
import HierarchyTrackingLayer import HierarchyTrackingLayer
public final class MediaRecordingPanelComponent: Component { public final class MediaRecordingPanelComponent: Component {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let audioRecorder: ManagedAudioRecorder? public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus? public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
public let isRecordingLocked: Bool
public let cancelFraction: CGFloat public let cancelFraction: CGFloat
public let inputInsets: UIEdgeInsets
public let insets: UIEdgeInsets public let insets: UIEdgeInsets
public let cancelAction: () -> Void
public init( public init(
theme: PresentationTheme,
strings: PresentationStrings,
audioRecorder: ManagedAudioRecorder?, audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?, videoRecordingStatus: InstantVideoControllerRecordingStatus?,
isRecordingLocked: Bool,
cancelFraction: CGFloat, cancelFraction: CGFloat,
insets: UIEdgeInsets inputInsets: UIEdgeInsets,
insets: UIEdgeInsets,
cancelAction: @escaping () -> Void
) { ) {
self.theme = theme
self.strings = strings
self.audioRecorder = audioRecorder self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus self.videoRecordingStatus = videoRecordingStatus
self.isRecordingLocked = isRecordingLocked
self.cancelFraction = cancelFraction self.cancelFraction = cancelFraction
self.inputInsets = inputInsets
self.insets = insets self.insets = insets
self.cancelAction = cancelAction
} }
public static func ==(lhs: MediaRecordingPanelComponent, rhs: MediaRecordingPanelComponent) -> Bool { public static func ==(lhs: MediaRecordingPanelComponent, rhs: MediaRecordingPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.audioRecorder !== rhs.audioRecorder { if lhs.audioRecorder !== rhs.audioRecorder {
return false return false
} }
if lhs.videoRecordingStatus !== rhs.videoRecordingStatus { if lhs.videoRecordingStatus !== rhs.videoRecordingStatus {
return false return false
} }
if lhs.isRecordingLocked != rhs.isRecordingLocked {
return false
}
if lhs.cancelFraction != rhs.cancelFraction { if lhs.cancelFraction != rhs.cancelFraction {
return false return false
} }
if lhs.inputInsets != rhs.inputInsets {
return false
}
if lhs.insets != rhs.insets { if lhs.insets != rhs.insets {
return false return false
} }
@ -50,13 +77,21 @@ public final class MediaRecordingPanelComponent: Component {
private var component: MediaRecordingPanelComponent? private var component: MediaRecordingPanelComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
public let vibrancyContainer: UIView
private let trackingLayer: HierarchyTrackingLayer private let trackingLayer: HierarchyTrackingLayer
private let indicator = ComponentView<Empty>() private let indicator = ComponentView<Empty>()
private let cancelContainerView: UIView private let cancelContainerView: UIView
private let vibrancyCancelContainerView: UIView
private let cancelIconView: UIImageView private let cancelIconView: UIImageView
private let vibrancyCancelIconView: UIImageView
private let vibrancyCancelText = ComponentView<Empty>()
private let cancelText = ComponentView<Empty>() private let cancelText = ComponentView<Empty>()
private let vibrancyCancelButtonText = ComponentView<Empty>()
private let cancelButtonText = ComponentView<Empty>()
private var cancelButton: HighlightableButton?
private let timerFont: UIFont private let timerFont: UIFont
private let timerText = ComponentView<Empty>() private let timerText = ComponentView<Empty>()
@ -68,16 +103,23 @@ public final class MediaRecordingPanelComponent: Component {
override init(frame: CGRect) { override init(frame: CGRect) {
self.trackingLayer = HierarchyTrackingLayer() self.trackingLayer = HierarchyTrackingLayer()
self.cancelIconView = UIImageView() self.cancelIconView = UIImageView()
self.vibrancyCancelIconView = UIImageView()
self.timerFont = Font.with(size: 15.0, design: .camera, traits: .monospacedNumbers) self.timerFont = Font.with(size: 15.0, design: .camera, traits: .monospacedNumbers)
self.vibrancyContainer = UIView()
self.cancelContainerView = UIView() self.cancelContainerView = UIView()
self.vibrancyCancelContainerView = UIView()
super.init(frame: frame) super.init(frame: frame)
self.layer.addSublayer(self.trackingLayer) self.layer.addSublayer(self.trackingLayer)
self.cancelContainerView.addSubview(self.cancelIconView) self.cancelContainerView.addSubview(self.cancelIconView)
self.vibrancyCancelContainerView.addSubview(self.vibrancyCancelIconView)
self.vibrancyContainer.addSubview(self.vibrancyCancelContainerView)
self.addSubview(self.cancelContainerView) self.addSubview(self.cancelContainerView)
self.trackingLayer.didEnterHierarchy = { [weak self] in self.trackingLayer.didEnterHierarchy = { [weak self] in
@ -97,6 +139,10 @@ public final class MediaRecordingPanelComponent: Component {
} }
private func updateAnimations() { private func updateAnimations() {
guard let component = self.component else {
return
}
if let indicatorView = self.indicator.view { if let indicatorView = self.indicator.view {
if indicatorView.layer.animation(forKey: "recording") == nil { if indicatorView.layer.animation(forKey: "recording") == nil {
let animation = CAKeyframeAnimation(keyPath: "opacity") let animation = CAKeyframeAnimation(keyPath: "opacity")
@ -109,7 +155,7 @@ public final class MediaRecordingPanelComponent: Component {
indicatorView.layer.add(animation, forKey: "recording") indicatorView.layer.add(animation, forKey: "recording")
} }
} }
if self.cancelContainerView.layer.animation(forKey: "recording") == nil { if !component.isRecordingLocked, self.cancelContainerView.layer.animation(forKey: "recording") == nil {
let animation = CAKeyframeAnimation(keyPath: "position.x") let animation = CAKeyframeAnimation(keyPath: "position.x")
animation.values = [-5.0 as NSNumber, 5.0 as NSNumber, 0.0 as NSNumber] animation.values = [-5.0 as NSNumber, 5.0 as NSNumber, 0.0 as NSNumber]
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber] animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
@ -119,26 +165,40 @@ public final class MediaRecordingPanelComponent: Component {
animation.repeatCount = Float.infinity animation.repeatCount = Float.infinity
self.cancelContainerView.layer.add(animation, forKey: "recording") self.cancelContainerView.layer.add(animation, forKey: "recording")
self.vibrancyCancelContainerView.layer.add(animation, forKey: "recording")
} }
} }
public func animateIn() { public func animateIn() {
guard let component = self.component else {
return
}
if let indicatorView = self.indicator.view { if let indicatorView = self.indicator.view {
indicatorView.layer.animatePosition(from: CGPoint(x: -20.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) indicatorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
indicatorView.layer.animatePosition(from: CGPoint(x: component.inputInsets.left - component.insets.left, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
} }
if let timerTextView = self.timerText.view { if let timerTextView = self.timerText.view {
timerTextView.layer.animatePosition(from: CGPoint(x: -20.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) timerTextView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
timerTextView.layer.animatePosition(from: CGPoint(x: component.inputInsets.left - component.insets.left, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
} }
self.cancelContainerView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.cancelContainerView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.vibrancyCancelContainerView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
} }
public func animateOut(dismissRecording: Bool, completion: @escaping () -> Void) { public func animateOut(transition: Transition, dismissRecording: Bool, completion: @escaping () -> Void) {
if let indicatorView = self.indicator.view as? LottieComponent.View { guard let component = self.component else {
if let _ = indicatorView.layer.animation(forKey: "recording") { completion()
let fromAlpha = indicatorView.layer.presentation()?.opacity ?? indicatorView.layer.opacity return
indicatorView.layer.removeAnimation(forKey: "recording") }
indicatorView.layer.animateAlpha(from: CGFloat(fromAlpha), to: 1.0, duration: 0.2)
if let indicatorView = self.indicator.view as? LottieComponent.View, let _ = indicatorView.layer.animation(forKey: "recording") {
let fromAlpha = indicatorView.layer.presentation()?.opacity ?? indicatorView.layer.opacity
indicatorView.layer.removeAnimation(forKey: "recording")
indicatorView.layer.animateAlpha(from: CGFloat(fromAlpha), to: 1.0, duration: 0.2)
}
if dismissRecording {
if let indicatorView = self.indicator.view as? LottieComponent.View {
indicatorView.playOnce(completion: { [weak indicatorView] in indicatorView.playOnce(completion: { [weak indicatorView] in
if let indicatorView { if let indicatorView {
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
@ -147,19 +207,35 @@ public final class MediaRecordingPanelComponent: Component {
completion() completion()
}) })
} else {
completion()
} }
} else { } else {
completion() if let indicatorView = self.indicator.view as? LottieComponent.View {
transition.setPosition(view: indicatorView, position: indicatorView.center.offsetBy(dx: component.inputInsets.left - component.insets.left, dy: 0.0))
transition.setAlpha(view: indicatorView, alpha: 0.0)
}
} }
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
if let timerTextView = self.timerText.view { if let timerTextView = self.timerText.view {
transition.setAlpha(view: timerTextView, alpha: 0.0) transition.setAlpha(view: timerTextView, alpha: 0.0, completion: { _ in
if !dismissRecording {
completion()
}
})
transition.setScale(view: timerTextView, scale: 0.001) transition.setScale(view: timerTextView, scale: 0.001)
transition.setPosition(view: timerTextView, position: timerTextView.center.offsetBy(dx: component.inputInsets.left - component.insets.left, dy: 0.0))
} }
transition.setAlpha(view: self.cancelContainerView, alpha: 0.0) transition.setAlpha(view: self.cancelContainerView, alpha: 0.0)
transition.setAlpha(view: self.vibrancyCancelContainerView, alpha: 0.0)
}
@objc private func cancelButtonPressed() {
guard let component = self.component else {
return
}
component.cancelAction()
} }
func update(component: MediaRecordingPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: MediaRecordingPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
@ -240,7 +316,7 @@ public final class MediaRecordingPanelComponent: Component {
if indicatorView.superview == nil { if indicatorView.superview == nil {
self.addSubview(indicatorView) self.addSubview(indicatorView)
} }
transition.setFrame(view: indicatorView, frame: CGRect(origin: CGPoint(x: 3.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - indicatorSize.height) * 0.5)), size: indicatorSize)) transition.setFrame(view: indicatorView, frame: CGRect(origin: CGPoint(x: 5.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - indicatorSize.height) * 0.5)), size: indicatorSize))
} }
let timerTextSize = self.timerText.update( let timerTextSize = self.timerText.update(
@ -254,25 +330,48 @@ public final class MediaRecordingPanelComponent: Component {
self.addSubview(timerTextView) self.addSubview(timerTextView)
timerTextView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5) timerTextView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
} }
let timerTextFrame = CGRect(origin: CGPoint(x: 38.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - timerTextSize.height) * 0.5)), size: timerTextSize) let timerTextFrame = CGRect(origin: CGPoint(x: 40.0, y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - timerTextSize.height) * 0.5)), size: timerTextSize)
transition.setPosition(view: timerTextView, position: CGPoint(x: timerTextFrame.minX, y: timerTextFrame.midY)) transition.setPosition(view: timerTextView, position: CGPoint(x: timerTextFrame.minX, y: timerTextFrame.midY))
timerTextView.bounds = CGRect(origin: CGPoint(), size: timerTextFrame.size) timerTextView.bounds = CGRect(origin: CGPoint(), size: timerTextFrame.size)
} }
if self.cancelIconView.image == nil { if self.cancelIconView.image == nil {
self.cancelIconView.image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate) let image = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.withRenderingMode(.alwaysTemplate)
self.cancelIconView.image = image
self.vibrancyCancelIconView.image = image
} }
self.cancelIconView.tintColor = UIColor(white: 1.0, alpha: 0.4) self.cancelIconView.tintColor = UIColor(white: 1.0, alpha: 0.3)
self.vibrancyCancelIconView.tintColor = .white
let cancelTextSize = self.cancelText.update( let cancelTextSize = self.cancelText.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(Text(text: "Slide to cancel", font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.4))), component: AnyComponent(Text(text: "Slide to cancel", font: Font.regular(15.0), color: UIColor(rgb: 0xffffff, alpha: 0.3))),
environment: {},
containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0)
)
let _ = self.vibrancyCancelText.update(
transition: .immediate,
component: AnyComponent(Text(text: "Slide to cancel", font: Font.regular(15.0), color: .white)),
environment: {},
containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0)
)
let cancelButtonTextSize = self.cancelButtonText.update(
transition: .immediate,
component: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0)
)
let _ = self.vibrancyCancelButtonText.update(
transition: .immediate,
component: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: .clear)),
environment: {}, environment: {},
containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0) containerSize: CGSize(width: max(30.0, availableSize.width - 100.0), height: 44.0)
) )
var textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelTextSize.width) * 0.5), y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - cancelTextSize.height) * 0.5)), size: cancelTextSize) var textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelTextSize.width) * 0.5), y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - cancelTextSize.height) * 0.5)), size: cancelTextSize)
let cancelButtonTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - cancelButtonTextSize.width) * 0.5), y: component.insets.top + floor((availableSize.height - component.insets.top - component.insets.bottom - cancelButtonTextSize.height) * 0.5)), size: cancelButtonTextSize)
let bandingStart: CGFloat = 0.0 let bandingStart: CGFloat = 0.0
let bandedOffset = abs(component.cancelFraction) - bandingStart let bandedOffset = abs(component.cancelFraction) - bandingStart
@ -282,18 +381,105 @@ public final class MediaRecordingPanelComponent: Component {
textFrame.origin.x -= mappedCancelFraction * 0.5 textFrame.origin.x -= mappedCancelFraction * 0.5
if component.isRecordingLocked {
if self.cancelContainerView.layer.animation(forKey: "recording") != nil {
if let presentation = self.cancelContainerView.layer.presentation() {
transition.animatePosition(view: self.cancelContainerView, from: presentation.position, to: CGPoint())
transition.animatePosition(view: self.vibrancyCancelContainerView, from: presentation.position, to: CGPoint())
}
self.cancelContainerView.layer.removeAnimation(forKey: "recording")
self.vibrancyCancelContainerView.layer.removeAnimation(forKey: "recording")
}
}
if let cancelTextView = self.cancelText.view { if let cancelTextView = self.cancelText.view {
if cancelTextView.superview == nil { if cancelTextView.superview == nil {
self.cancelContainerView.addSubview(cancelTextView) self.cancelContainerView.addSubview(cancelTextView)
} }
transition.setFrame(view: cancelTextView, frame: textFrame) transition.setPosition(view: cancelTextView, position: textFrame.center)
transition.setBounds(view: cancelTextView, bounds: CGRect(origin: CGPoint(), size: textFrame.size))
transition.setAlpha(view: cancelTextView, alpha: !component.isRecordingLocked ? 1.0 : 0.0)
transition.setScale(view: cancelTextView, scale: !component.isRecordingLocked ? 1.0 : 0.001)
} }
if let vibrancyCancelTextView = self.vibrancyCancelText.view {
if vibrancyCancelTextView.superview == nil {
self.vibrancyCancelContainerView.addSubview(vibrancyCancelTextView)
}
transition.setPosition(view: vibrancyCancelTextView, position: textFrame.center)
transition.setBounds(view: vibrancyCancelTextView, bounds: CGRect(origin: CGPoint(), size: textFrame.size))
transition.setAlpha(view: vibrancyCancelTextView, alpha: !component.isRecordingLocked ? 1.0 : 0.0)
transition.setScale(view: vibrancyCancelTextView, scale: !component.isRecordingLocked ? 1.0 : 0.001)
}
if let cancelButtonTextView = self.cancelButtonText.view {
if cancelButtonTextView.superview == nil {
self.cancelContainerView.addSubview(cancelButtonTextView)
}
transition.setPosition(view: cancelButtonTextView, position: cancelButtonTextFrame.center)
transition.setBounds(view: cancelButtonTextView, bounds: CGRect(origin: CGPoint(), size: cancelButtonTextFrame.size))
transition.setAlpha(view: cancelButtonTextView, alpha: component.isRecordingLocked ? 1.0 : 0.0)
transition.setScale(view: cancelButtonTextView, scale: component.isRecordingLocked ? 1.0 : 0.001)
}
if let vibrancyCancelButtonTextView = self.vibrancyCancelButtonText.view {
if vibrancyCancelButtonTextView.superview == nil {
self.vibrancyCancelContainerView.addSubview(vibrancyCancelButtonTextView)
}
transition.setPosition(view: vibrancyCancelButtonTextView, position: cancelButtonTextFrame.center)
transition.setBounds(view: vibrancyCancelButtonTextView, bounds: CGRect(origin: CGPoint(), size: cancelButtonTextFrame.size))
transition.setAlpha(view: vibrancyCancelButtonTextView, alpha: component.isRecordingLocked ? 1.0 : 0.0)
transition.setScale(view: vibrancyCancelButtonTextView, scale: component.isRecordingLocked ? 1.0 : 0.001)
}
if component.isRecordingLocked {
let cancelButton: HighlightableButton
if let current = self.cancelButton {
cancelButton = current
} else {
cancelButton = HighlightableButton()
self.cancelButton = cancelButton
self.addSubview(cancelButton)
cancelButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.cancelContainerView.alpha = 0.6
self.vibrancyCancelContainerView.alpha = 0.6
} else {
self.cancelContainerView.alpha = 1.0
self.vibrancyCancelContainerView.alpha = 1.0
self.cancelContainerView.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
self.vibrancyCancelContainerView.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), for: .touchUpInside)
}
cancelButton.frame = CGRect(origin: CGPoint(x: cancelButtonTextFrame.minX - 8.0, y: 0.0), size: CGSize(width: cancelButtonTextFrame.width + 8.0 * 2.0, height: availableSize.height))
} else if let cancelButton = self.cancelButton {
cancelButton.removeFromSuperview()
}
if let image = self.cancelIconView.image { if let image = self.cancelIconView.image {
transition.setFrame(view: self.cancelIconView, frame: CGRect(origin: CGPoint(x: textFrame.minX - 4.0 - image.size.width, y: textFrame.minY + floor((textFrame.height - image.size.height) * 0.5)), size: image.size)) let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - 4.0 - image.size.width, y: textFrame.minY + floor((textFrame.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.cancelIconView, position: iconFrame.center)
transition.setBounds(view: self.cancelIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setAlpha(view: self.cancelIconView, alpha: !component.isRecordingLocked ? 1.0 : 0.0)
transition.setScale(view: self.cancelIconView, scale: !component.isRecordingLocked ? 1.0 : 0.001)
transition.setPosition(view: self.vibrancyCancelIconView, position: iconFrame.center)
transition.setBounds(view: self.vibrancyCancelIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setAlpha(view: self.vibrancyCancelIconView, alpha: !component.isRecordingLocked ? 1.0 : 0.0)
transition.setScale(view: self.vibrancyCancelIconView, scale: !component.isRecordingLocked ? 1.0 : 0.001)
} }
self.updateAnimations() self.updateAnimations()
transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize return availableSize
} }
} }

View File

@ -8,6 +8,19 @@ import AccountContext
import TelegramPresentationData import TelegramPresentationData
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
private extension MessageInputActionButtonComponent.Mode {
var iconName: String? {
switch self {
case .delete:
return "Chat/Context Menu/Delete"
case .attach:
return "Chat/Input/Text/IconAttachment"
default:
return nil
}
}
}
public final class MessageInputActionButtonComponent: Component { public final class MessageInputActionButtonComponent: Component {
public enum Mode { public enum Mode {
case none case none
@ -15,6 +28,8 @@ public final class MessageInputActionButtonComponent: Component {
case apply case apply
case voiceInput case voiceInput
case videoInput case videoInput
case delete
case attach
} }
public enum Action { public enum Action {
@ -26,6 +41,8 @@ public final class MessageInputActionButtonComponent: Component {
public let action: (Mode, Action, Bool) -> Void public let action: (Mode, Action, Bool) -> Void
public let switchMediaInputMode: () -> Void public let switchMediaInputMode: () -> Void
public let updateMediaCancelFraction: (CGFloat) -> Void public let updateMediaCancelFraction: (CGFloat) -> Void
public let lockMediaRecording: () -> Void
public let stopAndPreviewMediaRecording: () -> Void
public let context: AccountContext public let context: AccountContext
public let theme: PresentationTheme public let theme: PresentationTheme
public let strings: PresentationStrings public let strings: PresentationStrings
@ -38,6 +55,8 @@ public final class MessageInputActionButtonComponent: Component {
action: @escaping (Mode, Action, Bool) -> Void, action: @escaping (Mode, Action, Bool) -> Void,
switchMediaInputMode: @escaping () -> Void, switchMediaInputMode: @escaping () -> Void,
updateMediaCancelFraction: @escaping (CGFloat) -> Void, updateMediaCancelFraction: @escaping (CGFloat) -> Void,
lockMediaRecording: @escaping () -> Void,
stopAndPreviewMediaRecording: @escaping () -> Void,
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
@ -49,6 +68,8 @@ public final class MessageInputActionButtonComponent: Component {
self.action = action self.action = action
self.switchMediaInputMode = switchMediaInputMode self.switchMediaInputMode = switchMediaInputMode
self.updateMediaCancelFraction = updateMediaCancelFraction self.updateMediaCancelFraction = updateMediaCancelFraction
self.lockMediaRecording = lockMediaRecording
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
self.context = context self.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@ -162,6 +183,12 @@ public final class MessageInputActionButtonComponent: Component {
break break
} }
} }
micButton.stopRecording = { [weak self] in
guard let self, let component = self.component else {
return
}
component.stopAndPreviewMediaRecording()
}
micButton.endRecording = { [weak self] sendMedia in micButton.endRecording = { [weak self] sendMedia in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
@ -173,6 +200,12 @@ public final class MessageInputActionButtonComponent: Component {
break break
} }
} }
micButton.updateLocked = { [weak self] _ in
guard let self, let component = self.component else {
return
}
component.lockMediaRecording()
}
micButton.switchMode = { [weak self] in micButton.switchMode = { [weak self] in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
@ -187,29 +220,33 @@ public final class MessageInputActionButtonComponent: Component {
} }
} }
if self.sendIconView.image == nil { if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName {
self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in if let iconName = component.mode.iconName {
context.clear(CGRect(origin: CGPoint(), size: size)) self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white)
context.setFillColor(UIColor.white.cgColor) } else {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
context.setBlendMode(.copy) context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor.clear.cgColor) context.setFillColor(UIColor.white.cgColor)
context.setLineWidth(2.0) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineCap(.round) context.setBlendMode(.copy)
context.setLineJoin(.round) context.setStrokeColor(UIColor.clear.cgColor)
context.setLineWidth(2.0)
context.translateBy(x: 5.45, y: 4.0) context.setLineCap(.round)
context.setLineJoin(.round)
context.saveGState()
context.translateBy(x: 4.0, y: 4.0) context.translateBy(x: 5.45, y: 4.0)
let _ = try? drawSvgPath(context, path: "M1,7 L7,1 L13,7 S ")
context.restoreGState() context.saveGState()
context.translateBy(x: 4.0, y: 4.0)
context.saveGState() let _ = try? drawSvgPath(context, path: "M1,7 L7,1 L13,7 S ")
context.translateBy(x: 10.0, y: 4.0) context.restoreGState()
let _ = try? drawSvgPath(context, path: "M1,16 V1 S ")
context.restoreGState() context.saveGState()
}) context.translateBy(x: 10.0, y: 4.0)
let _ = try? drawSvgPath(context, path: "M1,16 V1 S ")
context.restoreGState()
})
}
} }
var sendAlpha: CGFloat = 0.0 var sendAlpha: CGFloat = 0.0
@ -218,7 +255,7 @@ public final class MessageInputActionButtonComponent: Component {
switch component.mode { switch component.mode {
case .none: case .none:
break break
case .send, .apply: case .send, .apply, .attach, .delete:
sendAlpha = 1.0 sendAlpha = 1.0
case .videoInput, .voiceInput: case .videoInput, .voiceInput:
microphoneAlpha = 1.0 microphoneAlpha = 1.0
@ -248,7 +285,7 @@ public final class MessageInputActionButtonComponent: Component {
if previousComponent?.mode != component.mode { if previousComponent?.mode != component.mode {
switch component.mode { switch component.mode {
case .none, .send, .apply, .voiceInput: case .none, .send, .apply, .voiceInput, .attach, .delete:
micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate) micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate)
case .videoInput: case .videoInput:
micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate) micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate)

View File

@ -8,6 +8,7 @@ import BundleIconComponent
import AccountContext import AccountContext
import TelegramPresentationData import TelegramPresentationData
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import LottieComponent
public final class MessageInputPanelComponent: Component { public final class MessageInputPanelComponent: Component {
public enum Style { public enum Style {
@ -31,10 +32,16 @@ public final class MessageInputPanelComponent: Component {
public let presentController: (ViewController) -> Void public let presentController: (ViewController) -> Void
public let sendMessageAction: () -> Void public let sendMessageAction: () -> Void
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)? public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
public let lockMediaRecording: (() -> Void)?
public let stopAndPreviewMediaRecording: (() -> Void)?
public let discardMediaRecordingPreview: (() -> Void)?
public let attachmentAction: (() -> Void)? public let attachmentAction: (() -> Void)?
public let reactionAction: ((UIView) -> Void)? public let reactionAction: ((UIView) -> Void)?
public let audioRecorder: ManagedAudioRecorder? public let audioRecorder: ManagedAudioRecorder?
public let videoRecordingStatus: InstantVideoControllerRecordingStatus? public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
public let isRecordingLocked: Bool
public let recordedAudioPreview: ChatRecordedMediaPreview?
public let wasRecordingDismissed: Bool
public let displayGradient: Bool public let displayGradient: Bool
public let bottomInset: CGFloat public let bottomInset: CGFloat
@ -48,10 +55,16 @@ public final class MessageInputPanelComponent: Component {
presentController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void,
sendMessageAction: @escaping () -> Void, sendMessageAction: @escaping () -> Void,
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?, setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
lockMediaRecording: (() -> Void)?,
stopAndPreviewMediaRecording: (() -> Void)?,
discardMediaRecordingPreview: (() -> Void)?,
attachmentAction: (() -> Void)?, attachmentAction: (() -> Void)?,
reactionAction: ((UIView) -> Void)?, reactionAction: ((UIView) -> Void)?,
audioRecorder: ManagedAudioRecorder?, audioRecorder: ManagedAudioRecorder?,
videoRecordingStatus: InstantVideoControllerRecordingStatus?, videoRecordingStatus: InstantVideoControllerRecordingStatus?,
isRecordingLocked: Bool,
recordedAudioPreview: ChatRecordedMediaPreview?,
wasRecordingDismissed: Bool,
displayGradient: Bool, displayGradient: Bool,
bottomInset: CGFloat bottomInset: CGFloat
) { ) {
@ -64,10 +77,16 @@ public final class MessageInputPanelComponent: Component {
self.presentController = presentController self.presentController = presentController
self.sendMessageAction = sendMessageAction self.sendMessageAction = sendMessageAction
self.setMediaRecordingActive = setMediaRecordingActive self.setMediaRecordingActive = setMediaRecordingActive
self.lockMediaRecording = lockMediaRecording
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
self.discardMediaRecordingPreview = discardMediaRecordingPreview
self.attachmentAction = attachmentAction self.attachmentAction = attachmentAction
self.reactionAction = reactionAction self.reactionAction = reactionAction
self.audioRecorder = audioRecorder self.audioRecorder = audioRecorder
self.videoRecordingStatus = videoRecordingStatus self.videoRecordingStatus = videoRecordingStatus
self.isRecordingLocked = isRecordingLocked
self.wasRecordingDismissed = wasRecordingDismissed
self.recordedAudioPreview = recordedAudioPreview
self.displayGradient = displayGradient self.displayGradient = displayGradient
self.bottomInset = bottomInset self.bottomInset = bottomInset
} }
@ -97,6 +116,15 @@ public final class MessageInputPanelComponent: Component {
if lhs.videoRecordingStatus !== rhs.videoRecordingStatus { if lhs.videoRecordingStatus !== rhs.videoRecordingStatus {
return false return false
} }
if lhs.isRecordingLocked != rhs.isRecordingLocked {
return false
}
if lhs.wasRecordingDismissed != rhs.wasRecordingDismissed {
return false
}
if lhs.recordedAudioPreview !== rhs.recordedAudioPreview {
return false
}
if lhs.displayGradient != rhs.displayGradient { if lhs.displayGradient != rhs.displayGradient {
return false return false
} }
@ -123,13 +151,17 @@ public final class MessageInputPanelComponent: Component {
private let textFieldExternalState = TextFieldComponent.ExternalState() private let textFieldExternalState = TextFieldComponent.ExternalState()
private let attachmentButton = ComponentView<Empty>() private let attachmentButton = ComponentView<Empty>()
private var deleteMediaPreviewButton: ComponentView<Empty>?
private let inputActionButton = ComponentView<Empty>() private let inputActionButton = ComponentView<Empty>()
private let stickerButton = ComponentView<Empty>() private let stickerButton = ComponentView<Empty>()
private let reactionButton = ComponentView<Empty>() private let reactionButton = ComponentView<Empty>()
private var mediaRecordingVibrancyContainer: UIView
private var mediaRecordingPanel: ComponentView<Empty>? private var mediaRecordingPanel: ComponentView<Empty>?
private weak var dismissingMediaRecordingPanel: UIView? private weak var dismissingMediaRecordingPanel: UIView?
private var mediaPreviewPanel: ComponentView<Empty>?
private var currentMediaInputIsVoice: Bool = true private var currentMediaInputIsVoice: Bool = true
private var mediaCancelFraction: CGFloat = 0.0 private var mediaCancelFraction: CGFloat = 0.0
@ -145,6 +177,9 @@ public final class MessageInputPanelComponent: Component {
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect) let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
self.vibrancyEffectView = vibrancyEffectView self.vibrancyEffectView = vibrancyEffectView
self.mediaRecordingVibrancyContainer = UIView()
self.vibrancyEffectView.contentView.addSubview(self.mediaRecordingVibrancyContainer)
self.gradientView = UIImageView() self.gradientView = UIImageView()
self.bottomGradientView = UIView() self.bottomGradientView = UIView()
@ -181,20 +216,31 @@ public final class MessageInputPanelComponent: Component {
} }
} }
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result
}
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0) var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0)
if let _ = component.attachmentAction { if let _ = component.attachmentAction {
insets.left = 41.0 insets.left = 41.0
} }
if let _ = component.setMediaRecordingActive { if let _ = component.setMediaRecordingActive {
insets.right = 41.0 insets.right = 41.0
} }
let mediaInsets = UIEdgeInsets(top: insets.top, left: 7.0, bottom: insets.bottom, right: insets.right)
let baseFieldHeight: CGFloat = 40.0 let baseFieldHeight: CGFloat = 40.0
self.component = component self.component = component
self.state = state self.state = state
let hasMediaRecording = component.audioRecorder != nil || component.videoRecordingStatus != nil let hasMediaRecording = component.audioRecorder != nil || component.videoRecordingStatus != nil
let hasMediaEditing = component.recordedAudioPreview != nil
let topGradientHeight: CGFloat = 32.0 let topGradientHeight: CGFloat = 32.0
if self.gradientView.image == nil { if self.gradientView.image == nil {
@ -264,13 +310,20 @@ public final class MessageInputPanelComponent: Component {
} }
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height)) let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldFrame.size))
transition.setAlpha(view: self.vibrancyEffectView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0)
transition.setFrame(view: self.fieldBackgroundView, frame: fieldFrame) let fieldBackgroundFrame: CGRect
self.fieldBackgroundView.update(size: fieldFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition) if hasMediaRecording {
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: textFieldSize.height))
} else {
fieldBackgroundFrame = fieldFrame
}
let gradientFrame = CGRect(origin: CGPoint(x: 0.0, y: -topGradientHeight), size: CGSize(width: availableSize.width, height: topGradientHeight + fieldFrame.maxY + insets.bottom)) transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size))
transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame)
self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
let gradientFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX - fieldFrame.minX, y: -topGradientHeight), size: CGSize(width: availableSize.width - (fieldBackgroundFrame.minX - fieldFrame.minX), height: topGradientHeight + fieldBackgroundFrame.maxY + insets.bottom))
transition.setFrame(view: self.gradientView, frame: gradientFrame) transition.setFrame(view: self.gradientView, frame: gradientFrame)
transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset))) transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset)))
transition.setAlpha(view: self.gradientView, alpha: component.displayGradient ? 1.0 : 0.0) transition.setAlpha(view: self.gradientView, alpha: component.displayGradient ? 1.0 : 0.0)
@ -282,7 +335,7 @@ public final class MessageInputPanelComponent: Component {
} else { } else {
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0) placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
} }
let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: floor((fieldFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize) let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: floor((fieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize)
if let placeholderView = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view { if let placeholderView = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view {
if vibrancyPlaceholderView.superview == nil { if vibrancyPlaceholderView.superview == nil {
vibrancyPlaceholderView.layer.anchorPoint = CGPoint() vibrancyPlaceholderView.layer.anchorPoint = CGPoint()
@ -298,6 +351,9 @@ public final class MessageInputPanelComponent: Component {
} }
transition.setPosition(view: placeholderView, position: placeholderFrame.origin) transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
} }
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom) let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
@ -306,46 +362,151 @@ public final class MessageInputPanelComponent: Component {
if textFieldView.superview == nil { if textFieldView.superview == nil {
self.addSubview(textFieldView) self.addSubview(textFieldView)
} }
transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldFrame.minX, y: fieldFrame.maxY - textFieldSize.height), size: textFieldSize)) transition.setFrame(view: textFieldView, frame: CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize))
transition.setAlpha(view: textFieldView, alpha: (component.audioRecorder != nil || component.videoRecordingStatus != nil) ? 0.0 : 1.0) transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
} }
if let attachmentAction = component.attachmentAction { if component.attachmentAction != nil {
let attachmentButtonMode: MessageInputActionButtonComponent.Mode
attachmentButtonMode = .attach
let attachmentButtonSize = self.attachmentButton.update( let attachmentButtonSize = self.attachmentButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(MessageInputActionButtonComponent(
content: AnyComponent(BundleIconComponent( mode: attachmentButtonMode,
name: "Chat/Input/Text/IconAttachment", action: { [weak self] mode, action, sendAction in
tintColor: .white guard let self, let component = self.component, case .up = action else {
)), return
action: { }
attachmentAction()
} switch mode {
).minSize(CGSize(width: 41.0, height: baseFieldHeight))), case .delete:
break
case .attach:
component.attachmentAction?()
default:
break
}
},
switchMediaInputMode: {
},
updateMediaCancelFraction: { _ in
},
lockMediaRecording: {
},
stopAndPreviewMediaRecording: {
},
context: component.context,
theme: component.theme,
strings: component.strings,
presentController: component.presentController,
audioRecorder: nil,
videoRecordingStatus: nil
)),
environment: {}, environment: {},
containerSize: CGSize(width: 41.0, height: baseFieldHeight) containerSize: CGSize(width: 33.0, height: baseFieldHeight)
) )
if let attachmentButtonView = self.attachmentButton.view { if let attachmentButtonView = self.attachmentButton.view {
if attachmentButtonView.superview == nil { if attachmentButtonView.superview == nil {
self.addSubview(attachmentButtonView) self.addSubview(attachmentButtonView)
} }
transition.setFrame(view: attachmentButtonView, frame: CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize)) let attachmentButtonFrame = CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5) + (fieldBackgroundFrame.minX - fieldFrame.minX), y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize)
transition.setPosition(view: attachmentButtonView, position: attachmentButtonFrame.center)
transition.setBounds(view: attachmentButtonView, bounds: CGRect(origin: CGPoint(), size: attachmentButtonFrame.size))
transition.setAlpha(view: attachmentButtonView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
transition.setScale(view: attachmentButtonView, scale: hasMediaEditing ? 0.001 : 1.0)
} }
} }
if hasMediaEditing {
let deleteMediaPreviewButton: ComponentView<Empty>
var deleteMediaPreviewButtonTransition = transition
if let current = self.deleteMediaPreviewButton {
deleteMediaPreviewButton = current
} else {
if !transition.animation.isImmediate {
deleteMediaPreviewButtonTransition = .immediate
}
deleteMediaPreviewButton = ComponentView()
self.deleteMediaPreviewButton = deleteMediaPreviewButton
}
let buttonSize = CGSize(width: 40.0, height: 40.0)
let deleteMediaPreviewButtonFrame = CGRect(origin: CGPoint(x: 1.0 + (fieldBackgroundFrame.minX - fieldFrame.minX), y: 3.0 + floor((size.height - buttonSize.height) * 0.5)), size: CGSize(width: buttonSize.width, height: buttonSize.height))
let _ = deleteMediaPreviewButton.update(
transition: deleteMediaPreviewButtonTransition,
component: AnyComponent(Button(
content: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "BinBlue"),
color: .white,
startingPosition: .begin
)),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.discardMediaRecordingPreview?()
}
).minSize(buttonSize)),
environment: {},
containerSize: buttonSize
)
if let deleteMediaPreviewButtonView = deleteMediaPreviewButton.view {
if deleteMediaPreviewButtonView.superview == nil {
self.addSubview(deleteMediaPreviewButtonView)
transition.animateAlpha(view: deleteMediaPreviewButtonView, from: 0.0, to: 1.0)
transition.animatePosition(view: deleteMediaPreviewButtonView, from: CGPoint(x: mediaInsets.left - insets.left, y: 0.0), to: CGPoint(), additive: true)
}
deleteMediaPreviewButtonTransition.setFrame(view: deleteMediaPreviewButtonView, frame: deleteMediaPreviewButtonFrame)
}
} else if let deleteMediaPreviewButton = self.deleteMediaPreviewButton {
self.deleteMediaPreviewButton = nil
if let deleteMediaPreviewButtonView = deleteMediaPreviewButton.view {
if component.wasRecordingDismissed, let deleteMediaPreviewButtonView = deleteMediaPreviewButtonView as? Button.View, let animationView = deleteMediaPreviewButtonView.content as? LottieComponent.View {
if let attachmentButtonView = self.attachmentButton.view {
attachmentButtonView.isHidden = true
}
animationView.playOnce(completion: { [weak self, weak deleteMediaPreviewButtonView] in
guard let self, let deleteMediaPreviewButtonView else {
return
}
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
transition.setAlpha(view: deleteMediaPreviewButtonView, alpha: 0.0, completion: { [weak deleteMediaPreviewButtonView] _ in
deleteMediaPreviewButtonView?.removeFromSuperview()
})
transition.setScale(view: deleteMediaPreviewButtonView, scale: 0.001)
if let attachmentButtonView = self.attachmentButton.view {
attachmentButtonView.isHidden = false
transition.animateAlpha(view: attachmentButtonView, from: 0.0, to: attachmentButtonView.alpha)
transition.animateScale(view: attachmentButtonView, from: 0.001, to: 1.0)
}
})
} else {
transition.setAlpha(view: deleteMediaPreviewButtonView, alpha: 0.0, completion: { [weak deleteMediaPreviewButtonView] _ in
deleteMediaPreviewButtonView?.removeFromSuperview()
})
transition.setScale(view: deleteMediaPreviewButtonView, scale: 0.001)
}
}
}
let inputActionButtonMode: MessageInputActionButtonComponent.Mode let inputActionButtonMode: MessageInputActionButtonComponent.Mode
if case .editor = component.style { if case .editor = component.style {
inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none
} else { } else {
inputActionButtonMode = self.textFieldExternalState.hasText ? .send : (self.currentMediaInputIsVoice ? .voiceInput : .videoInput) if hasMediaEditing {
inputActionButtonMode = .send
} else {
inputActionButtonMode = self.textFieldExternalState.hasText ? .send : (self.currentMediaInputIsVoice ? .voiceInput : .videoInput)
}
} }
let inputActionButtonSize = self.inputActionButton.update( let inputActionButtonSize = self.inputActionButton.update(
transition: transition, transition: transition,
component: AnyComponent(MessageInputActionButtonComponent( component: AnyComponent(MessageInputActionButtonComponent(
mode: inputActionButtonMode, mode: inputActionButtonMode,
action: { [weak self] mode, action, sendAction in action: { [weak self] mode, action, sendAction in
guard let self else { guard let self, let component = self.component else {
return return
} }
@ -354,9 +515,11 @@ public final class MessageInputPanelComponent: Component {
break break
case .send: case .send:
if case .up = action { if case .up = action {
if case .text("") = self.getSendMessageInput() { if component.recordedAudioPreview != nil {
component.sendMessageAction()
} else if case .text("") = self.getSendMessageInput() {
} else { } else {
self.component?.sendMessageAction() component.sendMessageAction()
} }
} }
case .apply: case .apply:
@ -364,7 +527,9 @@ public final class MessageInputPanelComponent: Component {
self.component?.sendMessageAction() self.component?.sendMessageAction()
} }
case .voiceInput, .videoInput: case .voiceInput, .videoInput:
self.component?.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction) component.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction)
default:
break
} }
}, },
switchMediaInputMode: { [weak self] in switchMediaInputMode: { [weak self] in
@ -383,6 +548,18 @@ public final class MessageInputPanelComponent: Component {
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
} }
}, },
lockMediaRecording: { [weak self] in
guard let self, let component = self.component else {
return
}
component.lockMediaRecording?()
},
stopAndPreviewMediaRecording: { [weak self] in
guard let self, let component = self.component else {
return
}
component.stopAndPreviewMediaRecording?()
},
context: component.context, context: component.context,
theme: component.theme, theme: component.theme,
strings: component.strings, strings: component.strings,
@ -406,7 +583,7 @@ public final class MessageInputPanelComponent: Component {
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floorToScreenPixels((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize)) transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floorToScreenPixels((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
} }
var fieldIconNextX = fieldFrame.maxX - 2.0 var fieldIconNextX = fieldBackgroundFrame.maxX - 2.0
if case .story = component.style { if case .story = component.style {
let stickerButtonSize = self.stickerButton.update( let stickerButtonSize = self.stickerButton.update(
transition: transition, transition: transition,
@ -429,12 +606,12 @@ public final class MessageInputPanelComponent: Component {
if stickerButtonView.superview == nil { if stickerButtonView.superview == nil {
self.addSubview(stickerButtonView) self.addSubview(stickerButtonView)
} }
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldFrame.minY + floor((fieldFrame.height - stickerButtonSize.height) * 0.5)), size: stickerButtonSize) let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldBackgroundFrame.minY + floor((fieldBackgroundFrame.height - stickerButtonSize.height) * 0.5)), size: stickerButtonSize)
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center) transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size)) transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
transition.setAlpha(view: stickerButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording) ? 0.0 : 1.0) transition.setAlpha(view: stickerButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
transition.setScale(view: stickerButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) transition.setScale(view: stickerButtonView, scale: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.1 : 1.0)
fieldIconNextX -= stickerButtonSize.width + 2.0 fieldIconNextX -= stickerButtonSize.width + 2.0
} }
@ -462,19 +639,18 @@ public final class MessageInputPanelComponent: Component {
if reactionButtonView.superview == nil { if reactionButtonView.superview == nil {
self.addSubview(reactionButtonView) self.addSubview(reactionButtonView)
} }
let reactionIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - reactionButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - reactionButtonSize.height) * 0.5)), size: reactionButtonSize) let reactionIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - reactionButtonSize.width, y: fieldBackgroundFrame.minY + 1.0 + floor((fieldBackgroundFrame.height - reactionButtonSize.height) * 0.5)), size: reactionButtonSize)
transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center) transition.setPosition(view: reactionButtonView, position: reactionIconFrame.center)
transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size)) transition.setBounds(view: reactionButtonView, bounds: CGRect(origin: CGPoint(), size: reactionIconFrame.size))
transition.setAlpha(view: reactionButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording) ? 0.0 : 1.0) transition.setAlpha(view: reactionButtonView, alpha: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0)
transition.setScale(view: reactionButtonView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) transition.setScale(view: reactionButtonView, scale: (self.textFieldExternalState.hasText || hasMediaRecording || hasMediaEditing) ? 0.1 : 1.0)
fieldIconNextX -= reactionButtonSize.width + 2.0 fieldIconNextX -= reactionButtonSize.width + 2.0
} }
} }
self.fieldBackgroundView.updateColor(color: self.textFieldExternalState.isEditing || component.style == .editor ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition) self.fieldBackgroundView.updateColor(color: self.textFieldExternalState.isEditing || component.style == .editor ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
transition.setAlpha(view: self.fieldBackgroundView, alpha: hasMediaRecording ? 0.0 : 1.0)
if let placeholder = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view { if let placeholder = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view {
placeholder.isHidden = self.textFieldExternalState.hasText placeholder.isHidden = self.textFieldExternalState.hasText
vibrancyPlaceholderView.isHidden = placeholder.isHidden vibrancyPlaceholderView.isHidden = placeholder.isHidden
@ -483,7 +659,7 @@ public final class MessageInputPanelComponent: Component {
component.externalState.isEditing = self.textFieldExternalState.isEditing component.externalState.isEditing = self.textFieldExternalState.isEditing
component.externalState.hasText = self.textFieldExternalState.hasText component.externalState.hasText = self.textFieldExternalState.hasText
if component.audioRecorder != nil || component.videoRecordingStatus != nil { if hasMediaRecording {
if let dismissingMediaRecordingPanel = self.dismissingMediaRecordingPanel { if let dismissingMediaRecordingPanel = self.dismissingMediaRecordingPanel {
self.dismissingMediaRecordingPanel = nil self.dismissingMediaRecordingPanel = nil
transition.setAlpha(view: dismissingMediaRecordingPanel, alpha: 0.0, completion: { [weak dismissingMediaRecordingPanel] _ in transition.setAlpha(view: dismissingMediaRecordingPanel, alpha: 0.0, completion: { [weak dismissingMediaRecordingPanel] _ in
@ -504,10 +680,20 @@ public final class MessageInputPanelComponent: Component {
let _ = mediaRecordingPanel.update( let _ = mediaRecordingPanel.update(
transition: mediaRecordingPanelTransition, transition: mediaRecordingPanelTransition,
component: AnyComponent(MediaRecordingPanelComponent( component: AnyComponent(MediaRecordingPanelComponent(
theme: component.theme,
strings: component.strings,
audioRecorder: component.audioRecorder, audioRecorder: component.audioRecorder,
videoRecordingStatus: component.videoRecordingStatus, videoRecordingStatus: component.videoRecordingStatus,
isRecordingLocked: component.isRecordingLocked,
cancelFraction: self.mediaCancelFraction, cancelFraction: self.mediaCancelFraction,
insets: insets inputInsets: insets,
insets: mediaInsets,
cancelAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.setMediaRecordingActive?(false, false, false)
}
)), )),
environment: {}, environment: {},
containerSize: size containerSize: size
@ -516,17 +702,18 @@ public final class MessageInputPanelComponent: Component {
var animateIn = false var animateIn = false
if mediaRecordingPanelView.superview == nil { if mediaRecordingPanelView.superview == nil {
animateIn = true animateIn = true
self.insertSubview(mediaRecordingPanelView, at: 0) self.insertSubview(mediaRecordingPanelView, aboveSubview: self.fieldBackgroundView)
self.mediaRecordingVibrancyContainer.addSubview(mediaRecordingPanelView.vibrancyContainer)
} }
mediaRecordingPanelTransition.setFrame(view: mediaRecordingPanelView, frame: CGRect(origin: CGPoint(), size: size)) mediaRecordingPanelTransition.setFrame(view: mediaRecordingPanelView, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.mediaRecordingVibrancyContainer, frame: CGRect(origin: CGPoint(x: -fieldBackgroundFrame.minX, y: -fieldBackgroundFrame.minY), size: size))
if animateIn && !transition.animation.isImmediate { if animateIn && !transition.animation.isImmediate {
mediaRecordingPanelView.animateIn() mediaRecordingPanelView.animateIn()
} }
} }
if let attachmentButtonView = self.attachmentButton.view {
transition.setAlpha(view: attachmentButtonView, alpha: 0.0)
}
} else { } else {
if let mediaRecordingPanel = self.mediaRecordingPanel { if let mediaRecordingPanel = self.mediaRecordingPanel {
self.mediaRecordingPanel = nil self.mediaRecordingPanel = nil
@ -541,7 +728,11 @@ public final class MessageInputPanelComponent: Component {
self.dismissingMediaRecordingPanel = mediaRecordingPanel.view self.dismissingMediaRecordingPanel = mediaRecordingPanel.view
if let mediaRecordingPanelView = mediaRecordingPanel.view as? MediaRecordingPanelComponent.View { if let mediaRecordingPanelView = mediaRecordingPanel.view as? MediaRecordingPanelComponent.View {
mediaRecordingPanelView.animateOut(dismissRecording: true, completion: { [weak self, weak mediaRecordingPanelView] in let wasRecordingDismissed = component.wasRecordingDismissed
if wasRecordingDismissed, let attachmentButtonView = self.attachmentButton.view {
attachmentButtonView.isHidden = true
}
mediaRecordingPanelView.animateOut(transition: transition, dismissRecording: wasRecordingDismissed, completion: { [weak self, weak mediaRecordingPanelView] in
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
if let mediaRecordingPanelView = mediaRecordingPanelView { if let mediaRecordingPanelView = mediaRecordingPanelView {
@ -553,8 +744,10 @@ public final class MessageInputPanelComponent: Component {
guard let self else { guard let self else {
return return
} }
if self.mediaRecordingPanel == nil, let attachmentButtonView = self.attachmentButton.view { if wasRecordingDismissed, self.mediaRecordingPanel == nil, let attachmentButtonView = self.attachmentButton.view {
transition.setAlpha(view: attachmentButtonView, alpha: 1.0) attachmentButtonView.isHidden = false
transition.animateAlpha(view: attachmentButtonView, from: 0.0, to: attachmentButtonView.alpha)
transition.animateScale(view: attachmentButtonView, from: 0.001, to: 1.0) transition.animateScale(view: attachmentButtonView, from: 0.001, to: 1.0)
} }
}) })
@ -562,6 +755,57 @@ public final class MessageInputPanelComponent: Component {
} }
} }
if let recordedAudioPreview = component.recordedAudioPreview {
let mediaPreviewPanel: ComponentView<Empty>
var mediaPreviewPanelTransition = transition
if let current = self.mediaPreviewPanel {
mediaPreviewPanel = current
} else {
mediaPreviewPanelTransition = .immediate
mediaPreviewPanel = ComponentView()
self.mediaPreviewPanel = mediaPreviewPanel
}
let _ = mediaPreviewPanel.update(
transition: mediaPreviewPanelTransition,
component: AnyComponent(MediaPreviewPanelComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
mediaPreview: recordedAudioPreview,
insets: insets
)),
environment: {},
containerSize: size
)
if let mediaPreviewPanelView = mediaPreviewPanel.view as? MediaPreviewPanelComponent.View {
var animateIn = false
if mediaPreviewPanelView.superview == nil {
animateIn = true
self.insertSubview(mediaPreviewPanelView, aboveSubview: self.fieldBackgroundView)
self.mediaRecordingVibrancyContainer.addSubview(mediaPreviewPanelView.vibrancyContainer)
}
mediaPreviewPanelTransition.setFrame(view: mediaPreviewPanelView, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.mediaRecordingVibrancyContainer, frame: CGRect(origin: CGPoint(x: -fieldBackgroundFrame.minX, y: -fieldBackgroundFrame.minY), size: size))
if animateIn && !transition.animation.isImmediate {
mediaPreviewPanelView.animateIn()
}
}
} else {
if let mediaPreviewPanel = self.mediaPreviewPanel {
self.mediaPreviewPanel = nil
if let mediaPreviewPanelView = mediaPreviewPanel.view as? MediaPreviewPanelComponent.View {
mediaPreviewPanelView.animateOut(transition: transition, completion: { [weak mediaPreviewPanelView] in
mediaPreviewPanelView?.removeFromSuperview()
})
}
}
}
return size return size
} }
} }

View File

@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PeerInfoVisualMediaPaneNode",
module_name = "PeerInfoVisualMediaPaneNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ContextUI",
"//submodules/PhotoResources",
"//submodules/RadialStatusNode",
"//submodules/TelegramStringFormatting",
"//submodules/GridMessageSelectionNode",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/ListMessageItem",
"//submodules/ChatMessageInteractiveMediaBadge",
"//submodules/SparseItemGrid",
"//submodules/ShimmerEffect",
"//submodules/DirectMediaImageCache",
"//submodules/ComponentFlow",
"//submodules/TelegramNotices",
"//submodules/TelegramUIPreferences",
"//submodules/CheckNode",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/InvisibleInkDustNode",
"//submodules/MediaPickerUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -27,6 +27,55 @@ import ChatControllerInteraction
import InvisibleInkDustNode import InvisibleInkDustNode
import MediaPickerUI import MediaPickerUI
public enum PeerInfoPaneKey: Int32 {
case members
case stories
case media
case files
case music
case voice
case links
case gifs
case groupsInCommon
}
public struct PeerInfoStatusData: Equatable {
public var text: String
public var isActivity: Bool
public var key: PeerInfoPaneKey?
public init(
text: String,
isActivity: Bool,
key: PeerInfoPaneKey?
) {
self.text = text
self.isActivity = isActivity
self.key = key
}
}
public protocol PeerInfoPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get }
var parentController: ViewController? { get set }
var status: Signal<PeerInfoStatusData?, NoError> { get }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set }
var tabBarOffset: CGFloat { get }
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func scrollToTop() -> Bool
func transferVelocity(_ velocity: CGFloat)
func cancelPreviewGestures()
func findLoadedMessage(id: MessageId) -> Message?
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
func addToTransitionSurface(view: UIView)
func updateHiddenMedia()
func updateSelectedMessages(animated: Bool)
func ensureMessageIsVisible(id: MessageId)
}
private final class FrameSequenceThumbnailNode: ASDisplayNode { private final class FrameSequenceThumbnailNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
private let file: FileMediaReference private let file: FileMediaReference
@ -147,493 +196,6 @@ private final class VisualMediaItemInteraction {
} }
} }
/*private final class VisualMediaItemNode: ASDisplayNode {
private let context: AccountContext
private let interaction: VisualMediaItemInteraction
private var videoLayerFrameManager: SoftwareVideoLayerFrameManager?
private var sampleBufferLayer: SampleBufferLayer?
private var displayLink: ConstantDisplayLinkAnimator?
private var displayLinkTimestamp: Double = 0.0
private var frameSequenceThumbnailNode: FrameSequenceThumbnailNode?
private let containerNode: ContextControllerSourceNode
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private let imageNode: TransformImageNode
private var statusNode: RadialStatusNode
private let mediaBadgeNode: ChatMessageInteractiveMediaBadge
private var selectionNode: GridMessageSelectionNode?
private let fetchStatusDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var resourceStatus: MediaResourceStatus?
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
private var theme: PresentationTheme?
private var hasVisibility: Bool = false
init(context: AccountContext, interaction: VisualMediaItemInteraction) {
self.context = context
self.interaction = interaction
self.containerNode = ContextControllerSourceNode()
self.imageNode = TransformImageNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
let progressDiameter: CGFloat = 40.0
self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter)
self.statusNode.isUserInteractionEnabled = false
self.mediaBadgeNode = ChatMessageInteractiveMediaBadge()
self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 50.0, height: 50.0))
let shimmerNode = ShimmerEffectNode()
self.placeholderNode = shimmerNode
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.mediaBadgeNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let message = item.0.message else {
return
}
strongSelf.interaction.openMessageContextActions(message, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
}
}
deinit {
self.fetchStatusDisposable.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.imageNode.view.addGestureRecognizer(recognizer)
self.mediaBadgeNode.pressed = { [weak self] in
self?.progressPressed()
}
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
@objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state {
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
if case .tap = gesture {
if let (item, _, _, _) = self.item, let message = item.message {
var media: Media?
for value in message.media {
if let image = value as? TelegramMediaImage {
media = image
break
} else if let file = value as? TelegramMediaFile {
media = file
break
}
}
if let media = media {
if let file = media as? TelegramMediaFile {
if isMediaStreamable(message: message, media: file) {
self.interaction.openMessage(message)
} else {
self.progressPressed()
}
} else {
self.interaction.openMessage(message)
}
}
}
}
}
}
}
private func progressPressed() {
guard let message = self.item?.0.message else {
return
}
var media: Media?
for value in message.media {
if let image = value as? TelegramMediaImage {
media = image
break
} else if let file = value as? TelegramMediaFile {
media = file
break
}
}
if let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile {
switch resourceStatus {
case .Fetching:
messageMediaFileCancelInteractiveFetch(context: self.context, messageId: message.id, file: file)
case .Local:
self.interaction.openMessage(message)
case .Remote:
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: self.context, message: message, file: file, userInitiated: true).start())
}
}
}
func cancelPreviewGesture() {
self.containerNode.cancelGesture()
}
func update(size: CGSize, item: VisualMediaItem?, theme: PresentationTheme, synchronousLoad: Bool) {
if item === self.item?.0 && size == self.item?.2 {
return
}
self.theme = theme
var media: Media?
if let item = item, let message = item.message {
for value in message.media {
if let image = value as? TelegramMediaImage {
media = image
break
} else if let file = value as? TelegramMediaFile {
media = file
break
}
}
}
if let shimmerNode = self.placeholderNode {
shimmerNode.frame = CGRect(origin: CGPoint(), size: size)
if let (rect, size) = self.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
shapes.append(.rect(rect: CGRect(origin: CGPoint(), size: size)))
shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: size)
}
if let item = item, let message = item.message, let file = media as? TelegramMediaFile, file.isAnimated {
if self.videoLayerFrameManager == nil {
let sampleBufferLayer: SampleBufferLayer
if let current = self.sampleBufferLayer {
sampleBufferLayer = current
} else {
sampleBufferLayer = takeSampleBufferLayer()
self.sampleBufferLayer = sampleBufferLayer
self.imageNode.layer.addSublayer(sampleBufferLayer.layer)
}
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), layerHolder: sampleBufferLayer)
self.videoLayerFrameManager?.start()
}
} else {
if let sampleBufferLayer = self.sampleBufferLayer {
sampleBufferLayer.layer.removeFromSuperlayer()
self.sampleBufferLayer = nil
}
self.videoLayerFrameManager = nil
}
if let item = item, let message = item.message, let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) {
var mediaDimensions: CGSize?
if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions {
mediaDimensions = largestSize.cgSize
if let placeholderNode = self.placeholderNode, placeholderNode.supernode == nil {
self.containerNode.insertSubnode(placeholderNode, at: 0)
}
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.placeholderNode?.removeFromSupernode()
}
}
self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true)
self.fetchStatusDisposable.set(nil)
self.statusNode.transitionToState(.none, completion: { [weak self] in
self?.statusNode.isHidden = true
})
self.mediaBadgeNode.isHidden = true
self.resourceStatus = nil
} else if let file = media as? TelegramMediaFile, file.isVideo {
if let placeholderNode = self.placeholderNode, placeholderNode.supernode == nil {
self.containerNode.insertSubnode(placeholderNode, at: 0)
}
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.placeholderNode?.removeFromSupernode()
}
}
mediaDimensions = file.dimensions?.cgSize
self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad)
self.mediaBadgeNode.isHidden = file.isAnimated
self.resourceStatus = nil
self.item = (item, media, size, mediaDimensions)
self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: message.id, file: file)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, let (item, _, _, _) = strongSelf.item, let message = item.message {
strongSelf.resourceStatus = status
let isStreamable = isMediaStreamable(message: message, media: file)
var statusState: RadialStatusNodeState = .none
if isStreamable || file.isAnimated {
statusState = .none
} else {
switch status {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
case .Local:
statusState = .none
case .Remote:
statusState = .download(.white)
}
}
switch statusState {
case .none:
break
default:
strongSelf.statusNode.isHidden = false
}
strongSelf.statusNode.transitionToState(statusState, animated: true, completion: {
if let strongSelf = self {
if case .none = statusState {
strongSelf.statusNode.isHidden = true
}
}
})
if let duration = file.duration {
let durationString = stringForDuration(duration)
var badgeContent: ChatMessageInteractiveMediaBadgeContent?
var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
if isStreamable {
switch status {
case let .Fetching(_, progress):
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString))
mediaDownloadState = .compactFetching(progress: 0.0)
case .Local:
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
case .Remote:
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
mediaDownloadState = .compactRemote
}
} else {
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
}
strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)
}
}
}))
if self.statusNode.supernode == nil {
self.imageNode.addSubnode(self.statusNode)
}
} else {
self.mediaBadgeNode.isHidden = true
}
self.item = (item, media, size, mediaDimensions)
self.updateHiddenMedia()
} else {
if let placeholderNode = self.placeholderNode, placeholderNode.supernode == nil {
self.containerNode.insertSubnode(placeholderNode, at: 0)
}
}
let progressDiameter: CGFloat = 40.0
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - progressDiameter) / 2.0), y: floor((size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter))
self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 18.0 - 3.0), size: CGSize(width: 50.0, height: 50.0))
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: size)
if let (item, media, _, mediaDimensions) = self.item {
self.item = (item, media, size, mediaDimensions)
let imageFrame = CGRect(origin: CGPoint(), size: size)
self.containerNode.frame = imageFrame
self.imageNode.frame = imageFrame
if let sampleBufferLayer = self.sampleBufferLayer {
sampleBufferLayer.layer.frame = imageFrame
}
if let mediaDimensions = mediaDimensions {
let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))()
}
self.updateSelectionState(animated: false)
}
}
func updateIsVisible(_ isVisible: Bool) {
self.hasVisibility = isVisible
if let _ = self.videoLayerFrameManager {
let displayLink: ConstantDisplayLinkAnimator
if let current = self.displayLink {
displayLink = current
} else {
displayLink = ConstantDisplayLinkAnimator { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.videoLayerFrameManager?.tick(timestamp: strongSelf.displayLinkTimestamp)
strongSelf.displayLinkTimestamp += 1.0 / 30.0
}
displayLink.frameInterval = 2
self.displayLink = displayLink
}
}
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
/*if isVisible {
if let item = self.item?.0, let file = self.item?.1 as? TelegramMediaFile, !file.isAnimated {
if self.frameSequenceThumbnailNode == nil {
let frameSequenceThumbnailNode = FrameSequenceThumbnailNode(context: context, file: .message(message: MessageReference(item.message), media: file))
self.frameSequenceThumbnailNode = frameSequenceThumbnailNode
self.imageNode.addSubnode(frameSequenceThumbnailNode)
}
if let frameSequenceThumbnailNode = self.frameSequenceThumbnailNode {
let size = self.bounds.size
frameSequenceThumbnailNode.frame = CGRect(origin: CGPoint(), size: size)
frameSequenceThumbnailNode.updateLayout(size: size)
}
} else {
if let frameSequenceThumbnailNode = self.frameSequenceThumbnailNode {
self.frameSequenceThumbnailNode = nil
frameSequenceThumbnailNode.removeFromSupernode()
}
}
} else {
if let frameSequenceThumbnailNode = self.frameSequenceThumbnailNode {
self.frameSequenceThumbnailNode = nil
frameSequenceThumbnailNode.removeFromSupernode()
}
}*/
self.frameSequenceThumbnailNode?.updateIsPlaying(isVisible)
}
func tick() {
self.frameSequenceThumbnailNode?.tick()
}
func updateSelectionState(animated: Bool) {
if let (item, _, _, _) = self.item, let message = item.message, let theme = self.theme {
self.containerNode.isGestureEnabled = self.interaction.selectedMessageIds == nil
if let selectedIds = self.interaction.selectedMessageIds {
let selected = selectedIds.contains(message.id)
if let selectionNode = self.selectionNode {
selectionNode.updateSelected(selected, animated: animated)
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
} else {
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
if let strongSelf = self, let messageId = strongSelf.item?.0.message?.id {
var toggledValue = true
if let selectedMessageIds = strongSelf.interaction.selectedMessageIds, selectedMessageIds.contains(messageId) {
toggledValue = false
}
strongSelf.interaction.toggleSelection(messageId, toggledValue)
}
})
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.containerNode.addSubnode(selectionNode)
self.selectionNode = selectionNode
selectionNode.updateSelected(selected, animated: false)
if animated {
selectionNode.animateIn()
}
}
} else {
if let selectionNode = self.selectionNode {
self.selectionNode = nil
if animated {
selectionNode.animateOut { [weak selectionNode] in
selectionNode?.removeFromSupernode()
}
} else {
selectionNode.removeFromSupernode()
}
}
}
}
}
func transitionNode() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
let imageNode = self.imageNode
return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in
var statusNodeHidden = false
var accessoryHidden = false
if let strongSelf = self {
statusNodeHidden = strongSelf.statusNode.isHidden
accessoryHidden = strongSelf.mediaBadgeNode.isHidden
strongSelf.statusNode.isHidden = true
strongSelf.mediaBadgeNode.isHidden = true
}
let view = imageNode?.view.snapshotView(afterScreenUpdates: false)
if let strongSelf = self {
strongSelf.statusNode.isHidden = statusNodeHidden
strongSelf.mediaBadgeNode.isHidden = accessoryHidden
}
return (view, nil)
})
}
func updateHiddenMedia() {
if let (item, _, _, _) = self.item {
if let _ = self.interaction.hiddenMedia[item.id] {
self.isHidden = true
} else {
self.isHidden = false
}
} else {
self.isHidden = false
}
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
}
}
*/
private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor { private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor {
let messageId: MessageId let messageId: MessageId
override var id: AnyHashable { override var id: AnyHashable {
@ -1632,8 +1194,12 @@ private func tagMaskForType(_ type: PeerInfoVisualMediaPaneNode.ContentType) ->
} }
} }
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { public protocol PeerInfoScreenNodeProtocol: AnyObject {
enum ContentType { func displaySharedMediaFastScrollingTooltip()
}
public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
public enum ContentType {
case photoOrVideo case photoOrVideo
case photo case photo
case video case video
@ -1643,7 +1209,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
case music case music
} }
struct ZoomLevel { public struct ZoomLevel {
fileprivate var value: SparseItemGrid.ZoomLevel fileprivate var value: SparseItemGrid.ZoomLevel
init(_ value: SparseItemGrid.ZoomLevel) { init(_ value: SparseItemGrid.ZoomLevel) {
@ -1664,10 +1230,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private let chatLocation: ChatLocation private let chatLocation: ChatLocation
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?> private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
private let chatControllerInteraction: ChatControllerInteraction private let chatControllerInteraction: ChatControllerInteraction
private(set) var contentType: ContentType public private(set) var contentType: ContentType
private var contentTypePromise: ValuePromise<ContentType> private var contentTypePromise: ValuePromise<ContentType>
weak var parentController: ViewController? public weak var parentController: ViewController?
private let contextGestureContainerNode: ContextControllerSourceNode private let contextGestureContainerNode: ContextControllerSourceNode
private let itemGrid: SparseItemGrid private let itemGrid: SparseItemGrid
@ -1687,17 +1253,17 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private let ready = Promise<Bool>() private let ready = Promise<Bool>()
private var didSetReady: Bool = false private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> { public var isReady: Signal<Bool, NoError> {
return self.ready.get() return self.ready.get()
} }
private let statusPromise = Promise<PeerInfoStatusData?>(nil) private let statusPromise = Promise<PeerInfoStatusData?>(nil)
var status: Signal<PeerInfoStatusData?, NoError> { public var status: Signal<PeerInfoStatusData?, NoError> {
self.statusPromise.get() self.statusPromise.get()
} }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
var tabBarOffset: CGFloat { public var tabBarOffset: CGFloat {
return self.itemGrid.coveringInsetOffset return self.itemGrid.coveringInsetOffset
} }
@ -1712,11 +1278,11 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private var animationTimer: SwiftSignalKit.Timer? private var animationTimer: SwiftSignalKit.Timer?
private(set) var calendarSource: SparseMessageCalendar? public private(set) var calendarSource: SparseMessageCalendar?
private var listSource: SparseMessageList private var listSource: SparseMessageList
var openCurrentDate: (() -> Void)? public var openCurrentDate: (() -> Void)?
var paneDidScroll: (() -> Void)? public var paneDidScroll: (() -> Void)?
private let stateTag: MessageTags private let stateTag: MessageTags
private var storedStateDisposable: Disposable? private var storedStateDisposable: Disposable?
@ -1726,7 +1292,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private var presentationData: PresentationData private var presentationData: PresentationData
private var presentationDataDisposable: Disposable? private var presentationDataDisposable: Disposable?
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, contentType: ContentType, captureProtected: Bool) { public init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, contentType: ContentType, captureProtected: Bool) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.chatLocation = chatLocation self.chatLocation = chatLocation
@ -1889,9 +1455,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
let _ = ApplicationSpecificNotice.incrementSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager).start() let _ = ApplicationSpecificNotice.incrementSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager).start()
var currentNode: ASDisplayNode = strongSelf var currentNode: ASDisplayNode = strongSelf
var result: PeerInfoScreenNode? var result: PeerInfoScreenNodeProtocol?
while true { while true {
if let currentNode = currentNode as? PeerInfoScreenNode { if let currentNode = currentNode as? PeerInfoScreenNodeProtocol {
result = currentNode result = currentNode
break break
} else if let supernode = currentNode.supernode { } else if let supernode = currentNode.supernode {
@ -2221,7 +1787,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.presentationDataDisposable?.dispose() self.presentationDataDisposable?.dispose()
} }
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> { public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
guard let anchor = anchor as? VisualMediaHoleAnchor else { guard let anchor = anchor as? VisualMediaHoleAnchor else {
return .never() return .never()
} }
@ -2244,7 +1810,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func updateContentType(contentType: ContentType) { public func updateContentType(contentType: ContentType) {
if self.contentType == contentType { if self.contentType == contentType {
return return
} }
@ -2263,13 +1829,13 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true) self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true)
} }
func updateZoomLevel(level: ZoomLevel) { public func updateZoomLevel(level: ZoomLevel) {
self.itemGrid.setZoomLevel(level: level.value) self.itemGrid.setZoomLevel(level: level.value)
let _ = updateVisualMediaStoredState(engine: self.context.engine, peerId: self.peerId, messageTag: self.stateTag, state: VisualMediaStoredState(zoomLevel: level.rawValue)).start() let _ = updateVisualMediaStoredState(engine: self.context.engine, peerId: self.peerId, messageTag: self.stateTag, state: VisualMediaStoredState(zoomLevel: level.rawValue)).start()
} }
func ensureMessageIsVisible(id: MessageId) { public func ensureMessageIsVisible(id: MessageId) {
} }
private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) { private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) {
@ -2342,19 +1908,19 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func scrollToTop() -> Bool { public func scrollToTop() -> Bool {
return self.itemGrid.scrollToTop() return self.itemGrid.scrollToTop()
} }
func hitTestResultForScrolling() -> UIView? { public func hitTestResultForScrolling() -> UIView? {
return self.itemGrid.hitTestResultForScrolling() return self.itemGrid.hitTestResultForScrolling()
} }
func brieflyDisableTouchActions() { public func brieflyDisableTouchActions() {
self.itemGrid.brieflyDisableTouchActions() self.itemGrid.brieflyDisableTouchActions()
} }
func findLoadedMessage(id: MessageId) -> Message? { public func findLoadedMessage(id: MessageId) -> Message? {
guard let items = self.items else { guard let items = self.items else {
return nil return nil
} }
@ -2369,7 +1935,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return nil return nil
} }
func updateHiddenMedia() { public func updateHiddenMedia() {
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer else { guard let itemLayer = item.layer as? ItemLayer else {
return return
@ -2388,11 +1954,11 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func transferVelocity(_ velocity: CGFloat) { public func transferVelocity(_ velocity: CGFloat) {
self.itemGrid.transferVelocity(velocity) self.itemGrid.transferVelocity(velocity)
} }
func cancelPreviewGestures() { public func cancelPreviewGestures() {
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
guard let itemView = item.view as? ItemView else { guard let itemView = item.view as? ItemView else {
return return
@ -2403,7 +1969,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
var foundItemLayer: SparseItemGridLayer? var foundItemLayer: SparseItemGridLayer?
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer else { guard let itemLayer = item.layer as? ItemLayer else {
@ -2442,14 +2008,14 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return nil return nil
} }
func addToTransitionSurface(view: UIView) { public func addToTransitionSurface(view: UIView) {
self.itemGrid.addToTransitionSurface(view: view) self.itemGrid.addToTransitionSurface(view: view)
} }
private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>? private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
private var listSelectionGesture: MediaListSelectionRecognizer? private var listSelectionGesture: MediaListSelectionRecognizer?
override func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
@ -2594,7 +2160,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self.selectionScrollDisplayLink?.isPaused = false self.selectionScrollDisplayLink?.isPaused = false
} }
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: gestureRecognizer.view) let location = gestureRecognizer.location(in: gestureRecognizer.view)
if location.x < 44.0 { if location.x < 44.0 {
return false return false
@ -2602,7 +2168,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return true return true
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
otherGestureRecognizer.isEnabled = false otherGestureRecognizer.isEnabled = false
otherGestureRecognizer.isEnabled = true otherGestureRecognizer.isEnabled = true
@ -2612,7 +2178,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func updateSelectedMessages(animated: Bool) { public func updateSelectedMessages(animated: Bool) {
switch self.contentType { switch self.contentType {
case .files, .music, .voiceAndVideoMessages: case .files, .music, .voiceAndVideoMessages:
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
@ -2673,7 +2239,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
@ -2746,11 +2312,11 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
fixedItemHeight = nil fixedItemHeight = nil
} }
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: nil, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
} }
} }
func currentTopTimestamp() -> Int32? { public func currentTopTimestamp() -> Int32? {
var timestamp: Int32? var timestamp: Int32?
self.itemGrid.forEachVisibleItem { item in self.itemGrid.forEachVisibleItem { item in
guard let itemLayer = item.layer as? ItemLayer else { guard let itemLayer = item.layer as? ItemLayer else {
@ -2767,7 +2333,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return timestamp return timestamp
} }
func scrollToTimestamp(timestamp: Int32) { public func scrollToTimestamp(timestamp: Int32) {
if let items = self.items, !items.items.isEmpty { if let items = self.items, !items.items.isEmpty {
var previousIndex: Int? var previousIndex: Int?
for item in items.items { for item in items.items {
@ -2806,14 +2372,14 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func scrollToItem(index: Int) { public func scrollToItem(index: Int) {
guard let _ = self.items else { guard let _ = self.items else {
return return
} }
self.itemGrid.scrollToItem(at: index) self.itemGrid.scrollToItem(at: index)
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else { guard let result = super.hitTest(point, with: event) else {
return nil return nil
} }
@ -2826,21 +2392,21 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return result return result
} }
func availableZoomLevels() -> (decrement: ZoomLevel?, increment: ZoomLevel?) { public func availableZoomLevels() -> (decrement: ZoomLevel?, increment: ZoomLevel?) {
let levels = self.itemGrid.availableZoomLevels() let levels = self.itemGrid.availableZoomLevels()
return (levels.decrement.flatMap(ZoomLevel.init), levels.increment.flatMap(ZoomLevel.init)) return (levels.decrement.flatMap(ZoomLevel.init), levels.increment.flatMap(ZoomLevel.init))
} }
} }
final class VisualMediaStoredState: Codable { public final class VisualMediaStoredState: Codable {
let zoomLevel: Int32 public let zoomLevel: Int32
public init(zoomLevel: Int32) { public init(zoomLevel: Int32) {
self.zoomLevel = zoomLevel self.zoomLevel = zoomLevel
} }
} }
func visualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messageTag: MessageTags) -> Signal<VisualMediaStoredState?, NoError> { public func visualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messageTag: MessageTags) -> Signal<VisualMediaStoredState?, NoError> {
let key = ValueBoxKey(length: 8 + 4) let key = ValueBoxKey(length: 8 + 4)
key.setInt64(0, value: peerId.toInt64()) key.setInt64(0, value: peerId.toInt64())
key.setUInt32(8, value: messageTag.rawValue) key.setUInt32(8, value: messageTag.rawValue)
@ -2851,7 +2417,7 @@ func visualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messageTag:
} }
} }
func updateVisualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messageTag: MessageTags, state: VisualMediaStoredState?) -> Signal<Never, NoError> { public func updateVisualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messageTag: MessageTags, state: VisualMediaStoredState?) -> Signal<Never, NoError> {
let key = ValueBoxKey(length: 8 + 4) let key = ValueBoxKey(length: 8 + 4)
key.setInt64(0, value: peerId.toInt64()) key.setInt64(0, value: peerId.toInt64())
key.setUInt32(8, value: messageTag.rawValue) key.setUInt32(8, value: messageTag.rawValue)

View File

@ -50,6 +50,7 @@ swift_library(
"//submodules/ReactionSelectionNode", "//submodules/ReactionSelectionNode",
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/AvatarNode", "//submodules/AvatarNode",
"//submodules/ChatPresentationInterfaceState",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -36,14 +36,14 @@ private final class StoryContainerScreenComponent: Component {
let initialFocusedId: AnyHashable? let initialFocusedId: AnyHashable?
let initialContent: [StoryContentItemSlice] let initialContent: [StoryContentItemSlice]
let transitionIn: StoryContainerScreen.TransitionIn? let transitionIn: StoryContainerScreen.TransitionIn?
let transitionOut: (EnginePeer.Id) -> StoryContainerScreen.TransitionOut? let transitionOut: (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
init( init(
context: AccountContext, context: AccountContext,
initialFocusedId: AnyHashable?, initialFocusedId: AnyHashable?,
initialContent: [StoryContentItemSlice], initialContent: [StoryContentItemSlice],
transitionIn: StoryContainerScreen.TransitionIn?, transitionIn: StoryContainerScreen.TransitionIn?,
transitionOut: @escaping (EnginePeer.Id) -> StoryContainerScreen.TransitionOut? transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> StoryContainerScreen.TransitionOut?
) { ) {
self.context = context self.context = context
self.initialFocusedId = initialFocusedId self.initialFocusedId = initialFocusedId
@ -130,6 +130,7 @@ private final class StoryContainerScreenComponent: Component {
override init(frame: CGRect) { override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer() self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor.black.cgColor self.backgroundLayer.backgroundColor = UIColor.black.cgColor
self.backgroundLayer.zPosition = -1000.0
super.init(frame: frame) super.init(frame: frame)
@ -347,7 +348,7 @@ private final class StoryContainerScreenComponent: Component {
self.isAnimatingOut = true self.isAnimatingOut = true
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
if let component = self.component, let focusedItemSet = self.focusedItemSet, let peerId = focusedItemSet.base as? EnginePeer.Id, let itemSetView = self.visibleItemSetViews[focusedItemSet], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(peerId) { if let component = self.component, let focusedItemSet = self.focusedItemSet, let peerId = focusedItemSet.base as? EnginePeer.Id, let itemSetView = self.visibleItemSetViews[focusedItemSet], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let focusedItemId = itemSetComponentView.focusedItemId, let transitionOut = component.transitionOut(peerId, focusedItemId) {
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0) transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0)
@ -654,10 +655,12 @@ private final class StoryContainerScreenComponent: Component {
if let previousRotationFraction = itemSetView.rotationFraction { if let previousRotationFraction = itemSetView.rotationFraction {
let fromT = previousRotationFraction let fromT = previousRotationFraction
let toT = panFraction let toT = panFraction
itemSetTransition.setTransformAsKeyframes(view: itemSetView, transform: { sourceT in itemSetTransition.setTransformAsKeyframes(view: itemSetView, transform: { sourceT, isFinal in
let t = fromT * (1.0 - sourceT) + toT * sourceT let t = fromT * (1.0 - sourceT) + toT * sourceT
if abs((t + cubeAdditionalRotationFraction) - 0.0) < 0.0001 { if abs((t + cubeAdditionalRotationFraction) - 0.0) < 0.0001 {
return CATransform3DIdentity if isFinal {
return CATransform3DIdentity
}
} }
return calculateCubeTransform(rotationFraction: t + cubeAdditionalRotationFraction, sideAngle: sideAngle, cubeSize: itemFrame.size) return calculateCubeTransform(rotationFraction: t + cubeAdditionalRotationFraction, sideAngle: sideAngle, cubeSize: itemFrame.size)
@ -776,7 +779,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
initialFocusedId: AnyHashable?, initialFocusedId: AnyHashable?,
initialContent: [StoryContentItemSlice], initialContent: [StoryContentItemSlice],
transitionIn: TransitionIn?, transitionIn: TransitionIn?,
transitionOut: @escaping (EnginePeer.Id) -> TransitionOut? transitionOut: @escaping (EnginePeer.Id, AnyHashable) -> TransitionOut?
) { ) {
self.context = context self.context = context

View File

@ -250,27 +250,12 @@ public final class StoryItemSetContainerComponent: Component {
self.sendMessageContext.audioRecorderValue = audioRecorder self.sendMessageContext.audioRecorderValue = audioRecorder
self.component?.controller()?.lockOrientation = audioRecorder != nil self.component?.controller()?.lockOrientation = audioRecorder != nil
/*strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInputTextPanelState { panelState in
let isLocked = strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId
if let audioRecorder = audioRecorder {
if panelState.mediaRecordingState == nil {
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: isLocked))
}
} else {
if case .waitingForPreview = panelState.mediaRecordingState {
return panelState
}
return panelState.withUpdatedMediaRecordingState(nil)
}
return panelState
}
})*/
self.audioRecorderStatusDisposable?.dispose() self.audioRecorderStatusDisposable?.dispose()
self.audioRecorderStatusDisposable = nil self.audioRecorderStatusDisposable = nil
if let audioRecorder = audioRecorder { if let audioRecorder = audioRecorder {
self.sendMessageContext.wasRecordingDismissed = false
if !audioRecorder.beginWithTone { if !audioRecorder.beginWithTone {
HapticFeedback().impact(.light) HapticFeedback().impact(.light)
} }
@ -281,7 +266,7 @@ public final class StoryItemSetContainerComponent: Component {
return return
} }
if case .stopped = value { if case .stopped = value {
self.sendMessageContext.stopMediaRecorder() self.sendMessageContext.stopMediaRecording(view: self)
} }
}) })
} }
@ -300,15 +285,14 @@ public final class StoryItemSetContainerComponent: Component {
self.sendMessageContext.videoRecorderValue = videoRecorder self.sendMessageContext.videoRecorderValue = videoRecorder
if let videoRecorder = videoRecorder { if let videoRecorder = videoRecorder {
self.sendMessageContext.wasRecordingDismissed = false
HapticFeedback().impact(.light) HapticFeedback().impact(.light)
videoRecorder.onDismiss = { [weak self] isCancelled in videoRecorder.onDismiss = { [weak self] isCancelled in
guard let self else { guard let self else {
return return
} }
//self?.chatDisplayNode.updateRecordedMediaDeleted(isCancelled) self.sendMessageContext.wasRecordingDismissed = true
//self?.beginMediaRecordingRequestId += 1
//self?.lockMediaRecordingRequestId = nil
self.sendMessageContext.videoRecorder.set(.single(nil)) self.sendMessageContext.videoRecorder.set(.single(nil))
} }
videoRecorder.onStop = { [weak self] in videoRecorder.onStop = { [weak self] in
@ -327,9 +311,9 @@ public final class StoryItemSetContainerComponent: Component {
} }
self.component?.controller()?.present(videoRecorder, in: .window(.root)) self.component?.controller()?.present(videoRecorder, in: .window(.root))
/*if strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId { if self.sendMessageContext.isMediaRecordingLocked {
videoRecorder.lockVideo() videoRecorder.lockVideo()
}*/ }
} }
if let previousVideoRecorderValue { if let previousVideoRecorderValue {
@ -353,7 +337,17 @@ public final class StoryItemSetContainerComponent: Component {
} }
func isPointInsideContentArea(point: CGPoint) -> Bool { func isPointInsideContentArea(point: CGPoint) -> Bool {
return self.contentContainerView.frame.contains(point) if let inputPanelView = self.inputPanel.view {
if inputPanelView.frame.contains(point) {
return false
}
}
if self.contentContainerView.frame.contains(point) {
return true
}
return false
} }
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@ -539,7 +533,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.reactionItems != nil || 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)
} }
} }
} }
@ -813,6 +807,25 @@ public final class StoryItemSetContainerComponent: Component {
} }
self.sendMessageContext.setMediaRecordingActive(view: self, isActive: isActive, isVideo: isVideo, sendAction: sendAction) self.sendMessageContext.setMediaRecordingActive(view: self, isActive: isActive, isVideo: isVideo, sendAction: sendAction)
}, },
lockMediaRecording: { [weak self] in
guard let self else {
return
}
self.sendMessageContext.lockMediaRecording()
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
},
stopAndPreviewMediaRecording: { [weak self] in
guard let self else {
return
}
self.sendMessageContext.stopMediaRecording(view: self)
},
discardMediaRecordingPreview: { [weak self] in
guard let self else {
return
}
self.sendMessageContext.discardMediaRecordingPreview(view: self)
},
attachmentAction: { [weak self] in attachmentAction: { [weak self] in
guard let self else { guard let self else {
return return
@ -843,6 +856,9 @@ public final class StoryItemSetContainerComponent: Component {
}, },
audioRecorder: self.sendMessageContext.audioRecorderValue, audioRecorder: self.sendMessageContext.audioRecorderValue,
videoRecordingStatus: self.sendMessageContext.videoRecorderValue?.audioStatus, videoRecordingStatus: self.sendMessageContext.videoRecorderValue?.audioStatus,
isRecordingLocked: self.sendMessageContext.isMediaRecordingLocked,
recordedAudioPreview: self.sendMessageContext.recordedAudioPreview,
wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed,
displayGradient: component.inputHeight != 0.0, displayGradient: component.inputHeight != 0.0,
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset
)), )),
@ -1159,7 +1175,18 @@ public final class StoryItemSetContainerComponent: Component {
let reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0) let reactionsAnchorRect = CGRect(origin: CGPoint(x: inputPanelFrame.maxX - 40.0, y: inputPanelFrame.minY + 9.0), size: CGSize(width: 32.0, height: 32.0)).insetBy(dx: -4.0, dy: -4.0)
if let reactionItems = self.reactionItems, (self.displayReactions || self.inputPanelExternalState.isEditing) { var effectiveDisplayReactions = self.displayReactions
if self.inputPanelExternalState.isEditing && !self.inputPanelExternalState.hasText {
effectiveDisplayReactions = true
}
if self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil {
effectiveDisplayReactions = false
}
if self.sendMessageContext.recordedAudioPreview != nil {
effectiveDisplayReactions = false
}
if let reactionItems = self.reactionItems, effectiveDisplayReactions {
let reactionContextNode: ReactionContextNode let reactionContextNode: ReactionContextNode
var reactionContextNodeTransition = transition var reactionContextNodeTransition = transition
if let current = self.reactionContextNode { if let current = self.reactionContextNode {

View File

@ -3,6 +3,7 @@ import SwiftSignalKit
import TelegramCore import TelegramCore
import AccountContext import AccountContext
import Display import Display
import ComponentFlow
import MessageInputPanelComponent import MessageInputPanelComponent
import UndoUI import UndoUI
import AttachmentUI import AttachmentUI
@ -29,12 +30,14 @@ import TelegramPresentationData
import LegacyInstantVideoController import LegacyInstantVideoController
import TelegramPresentationData import TelegramPresentationData
import ShareController import ShareController
import ChatPresentationInterfaceState
final class StoryItemSetContainerSendMessage { final class StoryItemSetContainerSendMessage {
weak var attachmentController: AttachmentController? weak var attachmentController: AttachmentController?
var audioRecorderValue: ManagedAudioRecorder? var audioRecorderValue: ManagedAudioRecorder?
var audioRecorder = Promise<ManagedAudioRecorder?>() var audioRecorder = Promise<ManagedAudioRecorder?>()
var recordedAudioPreview: ChatRecordedMediaPreview?
var videoRecorderValue: InstantVideoController? var videoRecorderValue: InstantVideoController?
var tempVideoRecorderValue: InstantVideoController? var tempVideoRecorderValue: InstantVideoController?
@ -42,6 +45,9 @@ final class StoryItemSetContainerSendMessage {
let controllerNavigationDisposable = MetaDisposable() let controllerNavigationDisposable = MetaDisposable()
let enqueueMediaMessageDisposable = MetaDisposable() let enqueueMediaMessageDisposable = MetaDisposable()
private(set) var isMediaRecordingLocked: Bool = false
var wasRecordingDismissed: Bool = false
deinit { deinit {
self.controllerNavigationDisposable.dispose() self.controllerNavigationDisposable.dispose()
self.enqueueMediaMessageDisposable.dispose() self.enqueueMediaMessageDisposable.dispose()
@ -63,26 +69,38 @@ final class StoryItemSetContainerSendMessage {
return return
} }
switch inputPanelView.getSendMessageInput() { if let recordedAudioPreview = self.recordedAudioPreview {
case let .text(text): self.recordedAudioPreview = nil
if !text.isEmpty {
component.context.engine.messages.enqueueOutgoingMessage( let waveformBuffer = recordedAudioPreview.waveform.makeBitstream()
to: peerId,
replyTo: nil, let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedAudioPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedAudioPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedAudioPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
content: .text(text)
) let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start()
inputPanelView.clearSendMessageInput()
view.endEditing(true) view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
} else {
if let controller = component.controller() { switch inputPanelView.getSendMessageInput() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } case let .text(text):
controller.present(UndoOverlayController( if !text.isEmpty {
presentationData: presentationData, component.context.engine.messages.enqueueOutgoingMessage(
content: .succeed(text: "Message Sent"), to: peerId,
elevatedLayout: false, replyTo: nil,
animateInAsReplacement: false, content: .text(text)
action: { _ in return false } )
), in: .current) inputPanelView.clearSendMessageInput()
view.endEditing(true)
if let controller = component.controller() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Message Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
} }
} }
} }
@ -94,6 +112,8 @@ final class StoryItemSetContainerSendMessage {
isVideo: Bool, isVideo: Bool,
sendAction: Bool sendAction: Bool
) { ) {
self.isMediaRecordingLocked = false
guard let component = view.component else { guard let component = view.component else {
return return
} }
@ -167,6 +187,7 @@ final class StoryItemSetContainerSendMessage {
return return
} }
self.wasRecordingDismissed = !sendAction
self.audioRecorder.set(.single(nil)) self.audioRecorder.set(.single(nil))
guard let data else { guard let data else {
@ -205,7 +226,50 @@ final class StoryItemSetContainerSendMessage {
}) })
} }
func stopMediaRecorder() { func lockMediaRecording() {
self.isMediaRecordingLocked = true
}
func stopMediaRecording(view: StoryItemSetContainerComponent.View) {
if let audioRecorderValue = self.audioRecorderValue {
let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self, weak view] data in
guard let self, let view, let component = view.component else {
return
}
self.audioRecorder.set(.single(nil))
guard let data else {
return
}
if data.duration < 0.5 {
HapticFeedback().error()
} else if let waveform = data.waveform {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count))
component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData)
self.recordedAudioPreview = ChatRecordedMediaPreview(resource: resource, duration: Int32(data.duration), fileSize: Int32(data.compressedData.count), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))
view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
})
} else if let videoRecorderValue = self.videoRecorderValue {
if videoRecorderValue.stopVideo() {
/*self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInputTextPanelState { panelState in
return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false))
}
})*/
} else {
self.videoRecorder.set(.single(nil))
}
}
}
func discardMediaRecordingPreview(view: StoryItemSetContainerComponent.View) {
if self.recordedAudioPreview != nil {
self.recordedAudioPreview = nil
self.wasRecordingDismissed = true
view.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
} }
func performInlineAction(view: StoryItemSetContainerComponent.View, item: StoryActionsComponent.Item) { func performInlineAction(view: StoryItemSetContainerComponent.View, item: StoryActionsComponent.Item) {

View File

@ -8,7 +8,7 @@ import TelegramCore
import StoryContainerScreen import StoryContainerScreen
public enum StoryChatContent { public enum StoryChatContent {
public static func stories(context: AccountContext, storyList: StoryListContext, focusItem: Int64?) -> Signal<[StoryContentItemSlice], NoError> { public static func stories(context: AccountContext, storyList: StoryListContext, focusItem: Int32?) -> Signal<[StoryContentItemSlice], NoError> {
return storyList.state return storyList.state
|> map { state -> [StoryContentItemSlice] in |> map { state -> [StoryContentItemSlice] in
var itemSlices: [StoryContentItemSlice] = [] var itemSlices: [StoryContentItemSlice] = []
@ -59,7 +59,7 @@ public enum StoryChatContent {
} }
var sliceFocusedItemId: AnyHashable? var sliceFocusedItemId: AnyHashable?
if let focusItem, items.contains(where: { ($0.id.base as? Int64) == focusItem }) { if let focusItem, items.contains(where: { ($0.id.base as? Int32) == focusItem }) {
sliceFocusedItemId = AnyHashable(focusItem) sliceFocusedItemId = AnyHashable(focusItem)
} else { } else {
if let id = itemSet.items.first(where: { $0.id > itemSet.maxReadId })?.id { if let id = itemSet.items.first(where: { $0.id > itemSet.maxReadId })?.id {
@ -73,8 +73,8 @@ public enum StoryChatContent {
items: items, items: items,
totalCount: items.count, totalCount: items.count,
update: { requestedItemSet, itemId in update: { requestedItemSet, itemId in
var focusItem: Int64? var focusItem: Int32?
if let id = itemId.base as? Int64 { if let id = itemId.base as? Int32 {
focusItem = id focusItem = id
} }
return StoryChatContent.stories(context: context, storyList: storyList, focusItem: focusItem) return StoryChatContent.stories(context: context, storyList: storyList, focusItem: focusItem)

View File

@ -228,7 +228,7 @@ final class StoryItemContentComponent: Component {
} }
} }
#if DEBUG && false #if DEBUG// && false
let currentProgressTimerLimit: Double = 5 * 60.0 let currentProgressTimerLimit: Double = 5 * 60.0
#else #else
let currentProgressTimerLimit: Double = 5.0 let currentProgressTimerLimit: Double = 5.0

View File

@ -218,8 +218,7 @@ public final class StoryPeerListComponent: Component {
collapsedContentWidth += titleSpacing collapsedContentWidth += titleSpacing
} }
let collapseEndIndex = collapseStartIndex + Int(collapsedItemCount) let collapseEndIndex = collapseStartIndex + max(0, Int(collapsedItemCount) - 1)
let _ = collapseEndIndex
let titleOffset = collapsedContentWidth let titleOffset = collapsedContentWidth
collapsedContentWidth += titleSize.width collapsedContentWidth += titleSize.width
@ -307,13 +306,13 @@ public final class StoryPeerListComponent: Component {
var itemAlpha: CGFloat = 1.0 var itemAlpha: CGFloat = 1.0
if i >= collapseStartIndex && i <= (collapseStartIndex + 2) { if i >= collapseStartIndex && i <= collapseEndIndex {
if i != collapseStartIndex { if i != collapseStartIndex {
let regularLeftItemFrame = itemLayout.frame(at: i - 1) let regularLeftItemFrame = itemLayout.frame(at: i - 1)
let collapsedLeftItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularLeftItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularLeftItemFrame.height)) let collapsedLeftItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularLeftItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularLeftItemFrame.height))
leftItemFrame = regularLeftItemFrame.interpolate(to: collapsedLeftItemFrame, amount: component.collapseFraction) leftItemFrame = regularLeftItemFrame.interpolate(to: collapsedLeftItemFrame, amount: component.collapseFraction)
} }
if i != collapseStartIndex + 2 { if i != collapseEndIndex {
let regularRightItemFrame = itemLayout.frame(at: i - 1) let regularRightItemFrame = itemLayout.frame(at: i - 1)
let collapsedRightItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularRightItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularRightItemFrame.height)) let collapsedRightItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex - 1) * collapsedItemDistance, y: regularRightItemFrame.minY), size: CGSize(width: collapsedItemWidth, height: regularRightItemFrame.height))
rightItemFrame = regularRightItemFrame.interpolate(to: collapsedRightItemFrame, amount: component.collapseFraction) rightItemFrame = regularRightItemFrame.interpolate(to: collapsedRightItemFrame, amount: component.collapseFraction)
@ -414,7 +413,7 @@ public final class StoryPeerListComponent: Component {
if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) { if let myIndex = state.itemSets.firstIndex(where: { $0.peerId == component.context.account.peerId }) {
self.sortedItemSets.append(state.itemSets[myIndex]) self.sortedItemSets.append(state.itemSets[myIndex])
} }
for i in 0 ..< 4 { for i in 0 ..< 1 {
for itemSet in state.itemSets { for itemSet in state.itemSets {
if itemSet.peerId == component.context.account.peerId { if itemSet.peerId == component.context.account.peerId {
continue continue

View File

@ -1,22 +1,12 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "arrowleft.svg",
"scale" : "1x" "idiom" : "universal"
},
{
"idiom" : "universal",
"filename" : "ModernConversationAudioSlideToCancel@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ModernConversationAudioSlideToCancel@3x.png",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

View File

@ -0,0 +1,4 @@
<svg width="9" height="18" viewBox="0 0 9 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.43787 0.499507C8.71427 0.741355 8.74227 1.16148 8.50042 1.43788L1.88359 8.99997L8.50042 16.5621C8.74227 16.8385 8.71427 17.2586 8.43787 17.5004C8.16147 17.7423 7.74135 17.7143 7.4995 17.4379L0.499497 9.43788C0.280115 9.18715 0.280115 8.81279 0.499497 8.56206L7.4995 0.562064C7.74135 0.285666 8.16147 0.257658 8.43787 0.499507Z" fill="white" style="mix-blend-mode:overlay"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.43787 0.499507C8.71427 0.741355 8.74227 1.16148 8.50042 1.43788L1.88359 8.99997L8.50042 16.5621C8.74227 16.8385 8.71427 17.2586 8.43787 17.5004C8.16147 17.7423 7.74135 17.7143 7.4995 17.4379L0.499497 9.43788C0.280115 9.18715 0.280115 8.81279 0.499497 8.56206L7.4995 0.562064C7.74135 0.285666 8.16147 0.257658 8.43787 0.499507Z" fill="white" style="mix-blend-mode:overlay"/>
</svg>

After

Width:  |  Height:  |  Size: 953 B

View File

@ -1192,6 +1192,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
backgroundColor: isTranscriptionInProgress ? messageTheme.mediaInactiveControlColor : waveformColor, backgroundColor: isTranscriptionInProgress ? messageTheme.mediaInactiveControlColor : waveformColor,
foregroundColor: messageTheme.mediaActiveControlColor, foregroundColor: messageTheme.mediaActiveControlColor,
shimmerColor: isTranscriptionInProgress ? messageTheme.mediaActiveControlColor : nil, shimmerColor: isTranscriptionInProgress ? messageTheme.mediaActiveControlColor : nil,
style: .bottom,
samples: audioWaveform?.samples ?? Data(), samples: audioWaveform?.samples ?? Data(),
peak: audioWaveform?.peak ?? 0, peak: audioWaveform?.peak ?? 0,
status: strongSelf.playbackStatus.get(), status: strongSelf.playbackStatus.get(),
@ -1199,7 +1200,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(EngineMessage(message)) { if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(EngineMessage(message)) {
context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type) context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type)
} }
} },
updateIsSeeking: nil
)), )),
environment: {}, environment: {},
containerSize: scrubbingFrame.size containerSize: scrubbingFrame.size

View File

@ -12,6 +12,7 @@ import ItemListPeerItem
import MergeLists import MergeLists
import ItemListUI import ItemListUI
import ChatControllerInteraction import ChatControllerInteraction
import PeerInfoVisualMediaPaneNode
private struct GroupsInCommonListTransaction { private struct GroupsInCommonListTransaction {
let deletions: [ListViewDeleteItem] let deletions: [ListViewDeleteItem]

View File

@ -16,6 +16,7 @@ import ListMessageItem
import UndoUI import UndoUI
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
import ChatControllerInteraction import ChatControllerInteraction
import PeerInfoVisualMediaPaneNode
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext private let context: AccountContext

View File

@ -12,6 +12,7 @@ import ItemListPeerItem
import ItemListPeerActionItem import ItemListPeerActionItem
import MergeLists import MergeLists
import ItemListUI import ItemListUI
import PeerInfoVisualMediaPaneNode
private struct PeerMembersListTransaction { private struct PeerMembersListTransaction {
let deletions: [ListViewDeleteItem] let deletions: [ListViewDeleteItem]

View File

@ -12,6 +12,7 @@ import TelegramUIPreferences
import TelegramNotices import TelegramNotices
import AccountUtils import AccountUtils
import DeviceAccess import DeviceAccess
import PeerInfoVisualMediaPaneNode
enum PeerInfoUpdatingAvatar { enum PeerInfoUpdatingAvatar {
case none case none
@ -329,12 +330,6 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId
|> distinctUntilChanged |> distinctUntilChanged
} }
struct PeerInfoStatusData: Equatable {
var text: String
var isActivity: Bool
var key: PeerInfoPaneKey?
}
enum PeerInfoMembersData: Equatable { enum PeerInfoMembersData: Equatable {
case shortList(membersContext: PeerInfoMembersContext, members: [PeerInfoMember]) case shortList(membersContext: PeerInfoMembersContext, members: [PeerInfoMember])
case longList(PeerInfoMembersContext) case longList(PeerInfoMembersContext)
@ -681,6 +676,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
) )
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status -> PeerInfoScreenData in |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status -> PeerInfoScreenData in
var availablePanes = availablePanes var availablePanes = availablePanes
if peerView.peers[peerView.peerId] is TelegramUser {
availablePanes?.insert(.stories, at: 0)
}
if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData { if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData {
if cachedData.commonGroupCount != 0 { if cachedData.commonGroupCount != 0 {
availablePanes?.append(.groupsInCommon) availablePanes?.append(.groupsInCommon)

View File

@ -30,6 +30,7 @@ import ComponentDisplayAdapters
import ChatTitleView import ChatTitleView
import AppBundle import AppBundle
import AvatarVideoNode import AvatarVideoNode
import PeerInfoVisualMediaPaneNode
enum PeerInfoHeaderButtonKey: Hashable { enum PeerInfoHeaderButtonKey: Hashable {
case message case message

View File

@ -9,27 +9,7 @@ import TelegramCore
import AccountContext import AccountContext
import ContextUI import ContextUI
import ChatControllerInteraction import ChatControllerInteraction
import PeerInfoVisualMediaPaneNode
protocol PeerInfoPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get }
var parentController: ViewController? { get set }
var status: Signal<PeerInfoStatusData?, NoError> { get }
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set }
var tabBarOffset: CGFloat { get }
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func scrollToTop() -> Bool
func transferVelocity(_ velocity: CGFloat)
func cancelPreviewGestures()
func findLoadedMessage(id: MessageId) -> Message?
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
func addToTransitionSurface(view: UIView)
func updateHiddenMedia()
func updateSelectedMessages(animated: Bool)
func ensureMessageIsVisible(id: MessageId)
}
final class PeerInfoPaneWrapper { final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey let key: PeerInfoPaneKey
@ -53,17 +33,6 @@ final class PeerInfoPaneWrapper {
} }
} }
enum PeerInfoPaneKey: Int32 {
case members
case media
case files
case music
case voice
case links
case gifs
case groupsInCommon
}
final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
private let pressed: () -> Void private let pressed: () -> Void
@ -398,6 +367,15 @@ private final class PeerInfoPendingPane {
let captureProtected = data.peer?.isCopyProtectionEnabled ?? false let captureProtected = data.peer?.isCopyProtectionEnabled ?? false
let paneNode: PeerInfoPaneNode let paneNode: PeerInfoPaneNode
switch key { switch key {
case .stories:
let visualPaneNode = PeerInfoStoryPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected)
paneNode = visualPaneNode
visualPaneNode.openCurrentDate = {
openMediaCalendar()
}
visualPaneNode.paneDidScroll = {
paneDidScroll()
}
case .media: case .media:
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected) let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected)
paneNode = visualPaneNode paneNode = visualPaneNode
@ -977,6 +955,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String let title: String
switch key { switch key {
case .stories:
//TODO:localize
title = "Stories"
case .media: case .media:
title = presentationData.strings.PeerInfo_PaneMedia title = presentationData.strings.PeerInfo_PaneMedia
case .files: case .files:

View File

@ -86,6 +86,7 @@ import ChatControllerInteraction
import StorageUsageScreen import StorageUsageScreen
import AvatarEditorScreen import AvatarEditorScreen
import SendInviteLinkScreen import SendInviteLinkScreen
import PeerInfoVisualMediaPaneNode
enum PeerInfoAvatarEditingMode { enum PeerInfoAvatarEditingMode {
case generic case generic
@ -2016,7 +2017,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
return result return result
} }
final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodeProtocol, UIScrollViewDelegate {
private weak var controller: PeerInfoScreenImpl? private weak var controller: PeerInfoScreenImpl?
private let context: AccountContext private let context: AccountContext

View File

@ -15,6 +15,7 @@ import ListMessageItem
import ChatMessageInteractiveMediaBadge import ChatMessageInteractiveMediaBadge
import SoftwareVideo import SoftwareVideo
import ChatControllerInteraction import ChatControllerInteraction
import PeerInfoVisualMediaPaneNode
private final class FrameSequenceThumbnailNode: ASDisplayNode { private final class FrameSequenceThumbnailNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext

View File

@ -25,6 +25,7 @@ import LegacyCamera
import AvatarNode import AvatarNode
import LocalMediaResources import LocalMediaResources
import ShareWithPeersScreen import ShareWithPeersScreen
import ImageCompression
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData private var presentationData: PresentationData
@ -359,11 +360,16 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
switch mediaResult { switch mediaResult {
case let .image(image, dimensions, caption): case let .image(image, dimensions, caption):
if let data = image.jpegData(compressionQuality: 0.8) { var randomId: Int64 = 0
storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: caption?.string ?? "", entities: [], privacy: privacy) arc4random_buf(&randomId, 8)
Queue.mainQueue().after(0.2, { [weak chatListController] in let scaledSize = image.size.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0))
chatListController?.animateStoryUploadRipple() if let scaledImage = scaleImageToPixelSize(image: image, size: scaledSize) {
}) if let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.6) {
storyListContext.upload(media: .image(dimensions: dimensions, data: scaledImageData), text: caption?.string ?? "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.2, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
}
} }
case let .video(content, _, values, duration, dimensions, caption): case let .video(content, _, values, duration, dimensions, caption):
let adjustments: VideoMediaResourceAdjustments let adjustments: VideoMediaResourceAdjustments