Various fixes

This commit is contained in:
Ilya Laktyushin 2026-03-28 20:47:35 +01:00
parent 53d5425341
commit 6df559ff97
9 changed files with 71 additions and 189 deletions

View file

@ -5,6 +5,11 @@ import Display
import Markdown
public final class MultilineTextComponent: Component {
public final class CrossfadeTransition {
public init() {
}
}
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
@ -141,7 +146,7 @@ public final class MultilineTextComponent: Component {
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
//let previousText = self.attributedText?.string
let previousText = self.attributedText?.string
self.attributedText = attributedString
self.maximumNumberOfLines = component.maximumNumberOfLines
@ -160,7 +165,7 @@ public final class MultilineTextComponent: Component {
self.tapAttributeAction = component.tapAction
self.longTapAttributeAction = component.longTapAction
/*if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string {
if case let .curve(duration, _) = transition.animation, let _ = transition.userData(CrossfadeTransition.self), let previousText = previousText, previousText != attributedString.string {
if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
snapshotView.center = self.center
self.superview?.addSubview(snapshotView)
@ -170,7 +175,7 @@ public final class MultilineTextComponent: Component {
})
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
}*/
}
let size = self.updateLayout(availableSize)

View file

@ -162,7 +162,6 @@ private let placeholderFont = Font.regular(16.0)
private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode {
enum Mode {
case pictureInPicture
case airplay
}
private let iconNode: ASImageNode
@ -183,8 +182,6 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode
switch mode {
case .pictureInPicture:
text = strings.Embed_PlayingInPIP
case .airplay:
text = strings.Gallery_AirPlayPlaceholder
}
self.textNode.attributedText = NSAttributedString(string: text, font: placeholderFont, textColor: UIColor(rgb: 0x8e8e93))
@ -1868,7 +1865,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var hasMoreButton = false
var file: TelegramMediaFile?
for m in message.media {
if let m = m as? TelegramMediaFile, m.isVideo {
if let _ = m as? TelegramMediaImage {
hasMoreButton = true
} else if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
@ -2059,13 +2058,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
private func updateDisplayPlaceholder() {
self.updateDisplayPlaceholder(!(self.videoNode?.ownsContentNode ?? true) || self.isAirPlayActive)
self.updateDisplayPlaceholder(!(self.videoNode?.ownsContentNode ?? true))
}
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
if displayPlaceholder && !self.disablePictureInPicturePlaceholder {
if self.pictureInPictureNode == nil {
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings, mode: self.isAirPlayActive ? .airplay : .pictureInPicture)
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings, mode: .pictureInPicture)
pictureInPictureNode.isUserInteractionEnabled = false
self.pictureInPictureNode = pictureInPictureNode
self.insertSubnode(pictureInPictureNode, aboveSubnode: self.scrollNode)
@ -3772,16 +3771,24 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
})))
}
// if #available(iOS 11.0, *) {
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default)
// guard let strongSelf = self else {
// return
// }
// strongSelf.beginAirPlaySetup()
// })))
// }
if let (message, _, _) = strongSelf.contentInfo(), let image = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
let context = strongSelf.context
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
f(.default)
let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image))
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
guard let controller = strongSelf.galleryController() else {
return
}
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
})))
}
if let (message, _, _) = strongSelf.contentInfo() {
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
@ -3839,27 +3846,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return (items, topItems)
}
}
private var isAirPlayActive = false
private var externalVideoPlayer: ExternalVideoPlayer?
func beginAirPlaySetup() {
guard let content = self.item?.content as? NativeVideoContent else {
return
}
if #available(iOS 11.0, *) {
self.externalVideoPlayer = ExternalVideoPlayer(context: self.context, content: content)
self.externalVideoPlayer?.openRouteSelection()
self.externalVideoPlayer?.isActiveUpdated = { [weak self] isActive in
if let strongSelf = self {
if strongSelf.isAirPlayActive && !isActive {
strongSelf.externalVideoPlayer = nil
}
strongSelf.isAirPlayActive = isActive
strongSelf.updateDisplayPlaceholder()
}
}
}
}
@objc func openStickersButtonPressed() {
guard let content = self.item?.content as? NativeVideoContent else {

View file

@ -207,7 +207,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(TGMediaEdi
[super setItem:item synchronously:synchronously];
[self stopAndCleanupLivePhotoPlayback];
_livePhotoMode = TGMediaPickerGalleryResolvedLivePhotoMode(item.editingContext, item.editableMediaItem, (TGMediaAsset *)item.asset);
if ([item.asset isKindOfClass:[TGMediaAsset class]]) {
_livePhotoMode = TGMediaPickerGalleryResolvedLivePhotoMode(item.editingContext, item.editableMediaItem, (TGMediaAsset *)item.asset);
} else {
_livePhotoMode = TGMediaLivePhotoModeOff;
}
[self updatePaintingImage:[item.editingContext adjustmentsForItem:item.editableMediaItem]];
if (_entitiesView == nil) {
@ -722,7 +726,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(TGMediaEdi
_paintingImageView.hidden = (_paintingImageView.image == nil) || _livePhotoVideoView.alpha <= FLT_EPSILON;
_contentView.hidden = false;
_livePhotoMode = TGMediaPickerGalleryResolvedLivePhotoMode(self.item.editingContext, self.item.editableMediaItem, (TGMediaAsset *)self.item.asset);
if ([self.item.asset isKindOfClass:[TGMediaAsset class]]) {
_livePhotoMode = TGMediaPickerGalleryResolvedLivePhotoMode(self.item.editingContext, self.item.editableMediaItem, (TGMediaAsset *)self.item.asset);
} else {
_livePhotoMode = TGMediaLivePhotoModeOff;
}
[self updatePaintingImage:[self currentAdjustments]];
[self layoutEditedSubviews];

View file

@ -110,13 +110,7 @@ public func saveToCameraRoll(context: AccountContext, postbox: Postbox, userLoca
PHPhotoLibrary.shared().performChanges({
if isImage {
if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil)
} else {
if let image = UIImage(data: fileData) {
PHAssetChangeRequest.creationRequestForAsset(from: image)
}
}
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil)
}
} else {
if let _ = try? FileManager.default.copyItem(atPath: data.path, toPath: tempVideoPath) {

View file

@ -719,13 +719,21 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
self.fetchDisposable.dispose()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var result = super.hitTest(point, with: event)
if let mediaFrame = self.mediaFrame, mediaFrame.contains(point) {
result = self.view
}
return result
}
override func didLoad() {
super.didLoad()
self.highlightedBackgroundNode.view.tag = 0x1bad
self.separatorNode.view.tag = 0x3bad
}
@objc private func buttonPressed() {
guard !self.ignoreNextTap else {
self.ignoreNextTap = false

View file

@ -1419,10 +1419,10 @@ final class ComposePollScreenComponent: Component {
self.component = component
self.state = state
let topInset: CGFloat = 24.0
let topInset: CGFloat = 16.0
let bottomInset: CGFloat = 8.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
let sectionSpacing: CGFloat = 30.0
if themeUpdated {
self.backgroundColor = theme.list.blocksBackgroundColor
@ -1900,6 +1900,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedViewIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
@ -1939,6 +1940,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedMultipleIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
@ -1958,7 +1960,7 @@ final class ComposePollScreenComponent: Component {
}
}
self.state?.updated(transition: .spring(duration: 0.4))
self.state?.updated(transition: .spring(duration: 0.4).withUserData(MultilineTextComponent.CrossfadeTransition()))
})),
action: nil
))))
@ -1987,6 +1989,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedAddIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
@ -2027,6 +2030,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedRevoteIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
@ -2063,6 +2067,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedShuffleIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
@ -2099,6 +2104,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedQuizIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),
@ -2141,6 +2147,7 @@ final class ComposePollScreenComponent: Component {
)))
], alignment: .left, spacing: 4.0)),
verticalAlignment: .middle,
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(
Image(image: self.cachedDurationIcon, size: CGSize(width: 30.0, height: 30.0))
)), false),

View file

@ -762,10 +762,10 @@ final class ComposeTodoScreenComponent: Component {
self.component = component
self.state = state
let topInset: CGFloat = 24.0
let topInset: CGFloat = 8.0
let bottomInset: CGFloat = 8.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
let sectionSpacing: CGFloat = 30.0
if themeUpdated {
self.backgroundColor = theme.list.blocksBackgroundColor

View file

@ -496,17 +496,8 @@ public final class ListActionItemComponent: Component {
contentRightInset = customAccessorySizeValue.width + customAccessory.insets.left + customAccessory.insets.right
}
var contentInsets = component.contentInsets
switch component.style {
case .glass:
if contentInsets.top == 12.0 && contentInsets.bottom == 12.0 {
contentInsets.top = 16.0
contentInsets.bottom = 16.0
}
case .legacy:
break
}
let contentInsets = component.contentInsets
var contentHeight: CGFloat = 0.0
contentHeight += contentInsets.top
@ -531,11 +522,15 @@ public final class ListActionItemComponent: Component {
contentLeftInset = floor((availableSize.width - titleSize.width) / 2.0)
}
let titleY = contentHeight
contentHeight += titleSize.height
contentHeight += contentInsets.bottom
if case .glass = component.style, contentHeight < 52.0 {
contentHeight = 52.0
}
let titleY = floorToScreenPixels((contentHeight - titleSize.height) / 2.0)
if let iconValue = component.icon {
if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon {
self.icon = nil

View file

@ -1,121 +0,0 @@
import Foundation
import AVFoundation
import SwiftSignalKit
import UniversalMediaPlayer
import AccountContext
import AVKit
public class ExternalVideoPlayer: NSObject, AVRoutePickerViewDelegate {
private let context: AccountContext
let content: NativeVideoContent
let player: AVPlayer?
private var didPlayToEndTimeObserver: NSObjectProtocol?
private var timeObserver: Any?
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: false, progress: 0.0, display: true), soundEnabled: true)
private let _status = ValuePromise<MediaPlayerStatus>()
var status: Signal<MediaPlayerStatus, NoError> {
return self._status.get()
}
private var seekId: Int = 0
private weak var routePickerView: UIView?
public var isActiveUpdated: (Bool) -> Void = { _ in }
public init(context: AccountContext, content: NativeVideoContent) {
self.context = context
self.content = content
if let path = context.account.postbox.mediaBox.completedResourcePath(content.fileReference.media.resource, pathExtension: "mp4") {
let player = AVPlayer(url: URL(fileURLWithPath: path))
self.player = player
} else {
self.player = nil
}
super.init()
self.startObservingForAirPlayStatusChanges()
self.isActiveUpdated(self.player?.isExternalPlaybackActive ?? false)
if let player = self.player {
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
if let strongSelf = self {
strongSelf.player?.seek(to: CMTime(seconds: 0.0, preferredTimescale: 30))
strongSelf.play()
}
})
self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in
guard let strongSelf = self else {
return
}
strongSelf.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: strongSelf.statusValue.duration, dimensions: CGSize(), timestamp: CMTimeGetSeconds(time), baseRate: 1.0, seekId: strongSelf.seekId, status: strongSelf.statusValue.status, soundEnabled: true)
strongSelf._status.set(strongSelf.statusValue)
}
}
self._status.set(self.statusValue)
}
deinit {
if let timeObserver = self.timeObserver {
self.player?.removeTimeObserver(timeObserver)
}
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
}
self.stopObservingForAirPlayStatusChanges()
}
public func play() {
self.player?.play()
}
public func openRouteSelection() {
if #available(iOS 11.0, *) {
let routePickerView = AVRoutePickerView()
routePickerView.delegate = self
if #available(iOS 13.0, *) {
routePickerView.prioritizesVideoDevices = true
}
self.context.sharedContext.mainWindow?.viewController?.view.addSubview(routePickerView)
if let routePickerButton = routePickerView.subviews.first(where: { $0 is UIButton }) as? UIButton {
routePickerButton.sendActions(for: .touchUpInside)
}
}
}
@available(iOS 11.0, *)
public func routePickerViewDidEndPresentingRoutes(_ routePickerView: AVRoutePickerView) {
routePickerView.removeFromSuperview()
self.play()
}
private var observerContextAirplay = 1
func startObservingForAirPlayStatusChanges()
{
self.player?.addObserver(self, forKeyPath: #keyPath(AVPlayer.isExternalPlaybackActive), options: .new, context: &observerContextAirplay)
}
func stopObservingForAirPlayStatusChanges()
{
self.player?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.isExternalPlaybackActive))
}
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &observerContextAirplay {
self.isActiveUpdated(self.player?.isExternalPlaybackActive ?? false)
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}