diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 8eb464f0c1..2301f1fdab 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -2360,29 +2360,29 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { }) ) ), -// AnyComponentWithIdentity( -// id: "audio", -// component: AnyComponent( -// CameraButton( -// content: AnyComponentWithIdentity( -// id: "audio", -// component: AnyComponent( -// InteractiveStickerButtonContent( -// theme: theme, -// title: "AUDIO", -// iconName: "Media Editor/Audio", -// useOpaqueTheme: useOpaqueTheme, -// tintContainerView: self.tintContainerView -// ) -// ) -// ), -// action: { [weak self] in -// if let self { -// self.audioAction() -// } -// }) -// ) -// ), + AnyComponentWithIdentity( + id: "audio", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "audio", + component: AnyComponent( + InteractiveStickerButtonContent( + theme: theme, + title: "AUDIO", + iconName: "Media Editor/Audio", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.audioAction() + } + }) + ) + ), AnyComponentWithIdentity( id: "reaction", component: AnyComponent( diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index ef60d1f6ce..8e44ce05b9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -423,9 +423,16 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { longitude = 0.0 } return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), queryId: nil, resultId: nil)) - case let .mediaAreaSuggestedReaction(_, coordinates, reaction): + case let .mediaAreaSuggestedReaction(flags, coordinates, reaction): if let reaction = MessageReaction.Reaction(apiReaction: reaction) { - return .reaction(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), reaction: reaction) + var parsedFlags = MediaArea.ReactionFlags() + if (flags & (1 << 0)) != 0 { + parsedFlags.insert(.isDark) + } + if (flags & (1 << 1)) != 0 { + parsedFlags.insert(.isFlipped) + } + return .reaction(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), reaction: reaction, flags: parsedFlags) } else { return nil } @@ -446,8 +453,15 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea]) -> [Api.MediaArea] { } else { apiMediaAreas.append(.mediaAreaGeoPoint(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil))) } - case let .reaction(_, reaction): - apiMediaAreas.append(.mediaAreaSuggestedReaction(flags: 0, coordinates: inputCoordinates, reaction: reaction.apiReaction)) + case let .reaction(_, reaction, flags): + var apiFlags: Int32 = 0 + if flags.contains(.isDark) { + apiFlags |= (1 << 0) + } + if flags.contains(.isFlipped) { + apiFlags |= (1 << 1) + } + apiMediaAreas.append(.mediaAreaSuggestedReaction(flags: apiFlags, coordinates: inputCoordinates, reaction: reaction.apiReaction)) } } return apiMediaAreas diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index 4c68d0c9fe..3433b80d2d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -6,6 +6,7 @@ public enum MediaArea: Codable, Equatable { case type case coordinates case value + case flags } public struct Coordinates: Codable, Equatable { @@ -122,7 +123,23 @@ public enum MediaArea: Codable, Equatable { } case venue(coordinates: Coordinates, venue: Venue) - case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction) + case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags) + + public struct ReactionFlags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let isDark = ReactionFlags(rawValue: 1 << 0) + public static let isFlipped = ReactionFlags(rawValue: 1 << 1) + } + private enum MediaAreaType: Int32 { case venue @@ -143,7 +160,8 @@ public enum MediaArea: Codable, Equatable { case .reaction: let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let reaction = try container.decode(MessageReaction.Reaction.self, forKey: .value) - self = .reaction(coordinates: coordinates, reaction: reaction) + let flags = ReactionFlags(rawValue: try container.decodeIfPresent(Int32.self, forKey: .flags) ?? 0) + self = .reaction(coordinates: coordinates, reaction: reaction, flags: flags) } } @@ -155,10 +173,11 @@ public enum MediaArea: Codable, Equatable { try container.encode(MediaAreaType.venue.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(venue, forKey: .value) - case let .reaction(coordinates, reaction): + case let .reaction(coordinates, reaction, flags): try container.encode(MediaAreaType.reaction.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(reaction, forKey: .value) + try container.encode(flags.rawValue, forKey: .flags) } } } @@ -168,7 +187,7 @@ public extension MediaArea { switch self { case let .venue(coordinates, _): return coordinates - case let .reaction(coordinates, _): + case let .reaction(coordinates, _, _): return coordinates } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index f5dfe3d0df..6ba26378ce 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1422,6 +1422,7 @@ public class CameraScreen: ViewController { self.authorizationStatusDisposables.dispose() } + private var panGestureRecognizer: UIPanGestureRecognizer? private var pipPanGestureRecognizer: UIPanGestureRecognizer? override func didLoad() { super.didLoad() @@ -1435,6 +1436,7 @@ public class CameraScreen: ViewController { let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) panGestureRecognizer.delegate = self panGestureRecognizer.maximumNumberOfTouches = 1 + self.panGestureRecognizer = panGestureRecognizer self.previewContainerView.addGestureRecognizer(panGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) @@ -1598,6 +1600,8 @@ public class CameraScreen: ViewController { return false } return self.additionalPreviewContainerView.frame.contains(location) + } else if gestureRecognizer === self.panGestureRecognizer { + return true } return self.hasAppeared } @@ -1628,7 +1632,7 @@ public class CameraScreen: ViewController { case .changed: if case .none = self.cameraState.recording { if case .compact = layout.metrics.widthClass { - if translation.x < -10.0 || self.isDismissing { + if (translation.x < -10.0 || self.isDismissing) && self.hasAppeared { self.isDismissing = true let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.updateTransitionProgress(transitionFraction, transition: .immediate) @@ -1640,11 +1644,13 @@ public class CameraScreen: ViewController { } } case .ended, .cancelled: - let velocity = gestureRecognizer.velocity(in: self.view) - let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width - controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) - - self.isDismissing = false + if self.isDismissing { + let velocity = gestureRecognizer.velocity(in: self.view) + let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width + controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) + + self.isDismissing = false + } default: break } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index dd13df6953..63f612a6b8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -99,10 +99,18 @@ public enum CodableDrawingEntity: Equatable { ) ) case let .sticker(entity): - if case let .file(_, type) = entity.content, case let .reaction(reaction, _) = type { + if case let .file(_, type) = entity.content, case let .reaction(reaction, style) = type { + var flags: MediaArea.ReactionFlags = [] + if case .black = style { + flags.insert(.isDark) + } + if entity.mirrored { + flags.insert(.isFlipped) + } return .reaction( coordinates: coordinates, - reaction: reaction + reaction: reaction, + flags: flags ) } else { return nil diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 1ecba53fa5..3b0d9f5841 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -21,6 +21,7 @@ public struct MediaEditorPlayerState { public let framesCount: Int public let framesUpdateTimestamp: Double public let hasAudio: Bool + public let isAudioPlayerOnly: Bool } public final class MediaEditor { @@ -111,7 +112,7 @@ public final class MediaEditor { } public var resultIsVideo: Bool { - return self.player != nil || self.values.entities.contains(where: { $0.entity.isAnimated }) + return self.player != nil || self.audioPlayer != nil || self.values.entities.contains(where: { $0.entity.isAnimated }) } public var resultImage: UIImage? { @@ -123,16 +124,16 @@ public final class MediaEditor { } private let playerPromise = Promise() - private var playerPlaybackState: (Double, Double, Bool, Bool) = (0.0, 0.0, false, false) { + private var playerPlaybackState: (Double, Double, Bool, Bool, Bool) = (0.0, 0.0, false, false, false) { didSet { self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } } - private let playerPlaybackStatePromise = Promise<(Double, Double, Bool, Bool)>((0.0, 0.0, false, false)) + private let playerPlaybackStatePromise = Promise<(Double, Double, Bool, Bool, Bool)>((0.0, 0.0, false, false, false)) public var position: Signal { return self.playerPlaybackStatePromise.get() - |> map { _, position, _, _ -> Double in + |> map { _, position, _, _, _ -> Double in return position } } @@ -153,22 +154,44 @@ public final class MediaEditor { public func playerState(framesCount: Int) -> Signal { return self.playerPromise.get() |> mapToSignal { [weak self] player in - if let self, let asset = player?.currentItem?.asset { - return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount)) - |> map { values, durationAndPosition, framesAndUpdateTimestamp in - let (duration, position, isPlaying, hasAudio) = durationAndPosition - let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp - return MediaEditorPlayerState( - generationTimestamp: CACurrentMediaTime(), - duration: duration, - timeRange: values.videoTrimRange, - position: position, - isPlaying: isPlaying, - frames: frames, - framesCount: framesCount, - framesUpdateTimestamp: framesUpdateTimestamp, - hasAudio: hasAudio - ) + if let self, player != nil { + if player === self.player, let asset = player?.currentItem?.asset { + return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount)) + |> map { values, durationAndPosition, framesAndUpdateTimestamp in + let (duration, position, isPlaying, hasAudio, isAudioPlayerOnly) = durationAndPosition + let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp + return MediaEditorPlayerState( + generationTimestamp: CACurrentMediaTime(), + duration: duration, + timeRange: values.videoTrimRange, + position: position, + isPlaying: isPlaying, + frames: frames, + framesCount: framesCount, + framesUpdateTimestamp: framesUpdateTimestamp, + hasAudio: hasAudio, + isAudioPlayerOnly: isAudioPlayerOnly + ) + } + } else if player === self.audioPlayer { + return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get()) + |> map { values, durationAndPosition in + let (duration, position, isPlaying, _, _) = durationAndPosition + return MediaEditorPlayerState( + generationTimestamp: CACurrentMediaTime(), + duration: duration, + timeRange: values.audioTrackTrimRange, + position: position, + isPlaying: isPlaying, + frames: [], + framesCount: 0, + framesUpdateTimestamp: 0, + hasAudio: false, + isAudioPlayerOnly: true + ) + } + } else { + return .single(nil) } } else { return .single(nil) @@ -287,6 +310,8 @@ public final class MediaEditor { toolValues: [:], audioTrack: nil, audioTrackTrimRange: nil, + audioTrackStart: nil, + audioTrackVolume: nil, audioTrackSamples: nil ) } @@ -304,10 +329,10 @@ public final class MediaEditor { } if case let .asset(asset) = subject { - self.playerPlaybackState = (asset.duration, 0.0, false, false) + self.playerPlaybackState = (asset.duration, 0.0, false, false, false) self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } else if case let .video(_, _, _, _, _, duration) = subject { - self.playerPlaybackState = (duration, 0.0, false, true) + self.playerPlaybackState = (duration, 0.0, false, true, false) self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } } @@ -524,6 +549,13 @@ public final class MediaEditor { // self.maybeGeneratePersonSegmentation(image) } + if let audioTrack = self.values.audioTrack { + self.setAudioTrack(audioTrack) + self.setAudioTrackVolume(self.values.audioTrackVolume) + self.setAudioTrackTrimRange(self.values.audioTrackTrimRange, apply: true) + self.setAudioTrackStart(self.values.audioTrackStart) + } + if let player { player.isMuted = self.values.videoIsMuted if let trimRange = self.values.videoTrimRange { @@ -535,40 +567,12 @@ public final class MediaEditor { self.initialSeekPosition = nil player.seek(to: CMTime(seconds: initialSeekPosition, preferredTimescale: CMTimeScale(1000)), toleranceBefore: .zero, toleranceAfter: .zero) } - self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in - guard let self, let duration = player.currentItem?.duration.seconds else { - return - } - var hasAudio = false - if let audioTracks = player.currentItem?.asset.tracks(withMediaType: .audio) { - hasAudio = !audioTracks.isEmpty - } - if time.seconds > 20000 { - - } else { - self.playerPlaybackState = (duration, time.seconds, player.rate > 0.0, hasAudio) - } - } - self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in - if let self { - let start = self.values.videoTrimRange?.lowerBound ?? 0.0 - self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000))) - self.additionalPlayer?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000))) - self.audioPlayer?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000))) - self.onPlaybackAction(.seek(start)) - - self.player?.play() - self.additionalPlayer?.play() - self.audioPlayer?.play() - - Queue.mainQueue().justDispatch { - self.onPlaybackAction(.play) - } - } - }) + + self.setupTimeObservers() Queue.mainQueue().justDispatch { player.playImmediately(atRate: 1.0) additionalPlayer?.playImmediately(atRate: 1.0) + self.audioPlayer?.playImmediately(atRate: 1.0) self.onPlaybackAction(.play) self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) } @@ -577,6 +581,62 @@ public final class MediaEditor { }) } + private func setupTimeObservers() { + var observedPlayer = self.player + var isAudioPlayerOnly = false + if observedPlayer == nil { + observedPlayer = self.audioPlayer + if observedPlayer != nil { + isAudioPlayerOnly = true + } + } + guard let observedPlayer else { + return + } + + if self.timeObserver == nil { + self.timeObserver = observedPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self, weak observedPlayer] time in + guard let self, let observedPlayer, let duration = observedPlayer.currentItem?.duration.seconds else { + return + } + var hasAudio = false + if let audioTracks = observedPlayer.currentItem?.asset.tracks(withMediaType: .audio) { + hasAudio = !audioTracks.isEmpty + } + if time.seconds > 20000 { + + } else { + self.playerPlaybackState = (duration, time.seconds, observedPlayer.rate > 0.0, hasAudio, isAudioPlayerOnly) + } + } + } + + if self.didPlayToEndTimeObserver == nil { + self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observedPlayer.currentItem, queue: nil, using: { [weak self] notification in + if let self { + let start = self.values.videoTrimRange?.lowerBound ?? 0.0 + let targetTime = CMTime(seconds: start, preferredTimescale: CMTimeScale(1000)) + self.player?.seek(to: targetTime) + self.additionalPlayer?.seek(to: targetTime) + self.audioPlayer?.seek(to: self.audioTime(for: targetTime)) + self.onPlaybackAction(.seek(start)) + + self.player?.play() + self.additionalPlayer?.play() + self.audioPlayer?.play() + + Queue.mainQueue().justDispatch { + self.onPlaybackAction(.play) + } + } + }) + } + } + + private func setupDidPlayToEndObserver() { + + } + public func attachPreviewView(_ previewView: MediaEditorPreviewView) { self.previewView?.renderer = nil @@ -666,12 +726,12 @@ public final class MediaEditor { private var targetTimePosition: (CMTime, Bool)? private var updatingTimePosition = false public func seek(_ position: Double, andPlay play: Bool) { - guard let player = self.player else { + if self.player == nil && self.audioPlayer == nil { self.initialSeekPosition = position return } if !play { - player.pause() + self.player?.pause() self.additionalPlayer?.pause() self.audioPlayer?.pause() self.onPlaybackAction(.pause) @@ -684,7 +744,7 @@ public final class MediaEditor { } } if play { - player.play() + self.player?.play() self.additionalPlayer?.play() self.audioPlayer?.play() self.onPlaybackAction(.play) @@ -705,12 +765,21 @@ public final class MediaEditor { completion() } }) + self.additionalPlayer?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero) - self.audioPlayer?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero) + self.audioPlayer?.seek(to: self.audioTime(for: targetPosition), toleranceBefore: .zero, toleranceAfter: .zero) + } + + private func audioTime(for time: CMTime) -> CMTime { + let time = time.seconds + let offsettedTime = time - (self.values.videoTrimRange?.lowerBound ?? 0.0) + (self.values.audioTrackTrimRange?.lowerBound ?? 0.0) - (self.values.audioTrackStart ?? 0.0) + + return CMTime(seconds: offsettedTime, preferredTimescale: CMTimeScale(1000.0)) } public var isPlaying: Bool { - return (self.player?.rate ?? 0.0) > 0.0 + let effectivePlayer = self.player ?? self.audioPlayer + return (effectivePlayer?.rate ?? 0.0) > 0.0 } public func togglePlayback() { @@ -736,11 +805,22 @@ public final class MediaEditor { let cmVTime = CMTimeMakeWithSeconds(time, preferredTimescale: 1000000) let futureTime = CMTimeAdd(cmHostTime, cmVTime) - let itemTime = self.player?.currentItem?.currentTime() ?? .invalid - - self.player?.setRate(rate, time: itemTime, atHostTime: futureTime) - self.additionalPlayer?.setRate(rate, time: itemTime, atHostTime: futureTime) - self.audioPlayer?.setRate(rate, time: itemTime, atHostTime: futureTime) + if self.player == nil, let audioPlayer = self.audioPlayer { + let itemTime = audioPlayer.currentItem?.currentTime() ?? .invalid + audioPlayer.setRate(rate, time: itemTime, atHostTime: futureTime) + } else { + let itemTime = self.player?.currentItem?.currentTime() ?? .invalid + let audioTime: CMTime + if itemTime == .invalid { + audioTime = .invalid + } else { + audioTime = self.audioTime(for: itemTime) + } + + self.player?.setRate(rate, time: itemTime, atHostTime: futureTime) + self.additionalPlayer?.setRate(rate, time: itemTime, atHostTime: futureTime) + self.audioPlayer?.setRate(rate, time: audioTime, atHostTime: futureTime) + } if rate > 0.0 { self.onPlaybackAction(.play) @@ -762,18 +842,33 @@ public final class MediaEditor { return } self.updatingTimePosition = true - self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in - if let self { - if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition { - self.updatingTimePosition = false - self.targetTimePosition = nil - } else { - self.updateVideoTimePosition() + + if self.player == nil, let audioPlayer = self.audioPlayer { + audioPlayer.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in + if let self { + if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition { + self.updatingTimePosition = false + self.targetTimePosition = nil + } else { + self.updateVideoTimePosition() + } } - } - }) - self.additionalPlayer?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero) - self.audioPlayer?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero) + }) + } else { + self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in + if let self { + if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition { + self.updatingTimePosition = false + self.targetTimePosition = nil + } else { + self.updateVideoTimePosition() + } + } + }) + + self.additionalPlayer?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero) + self.audioPlayer?.seek(to: self.audioTime(for: targetPosition), toleranceBefore: .zero, toleranceAfter: .zero) + } self.onPlaybackAction(.seek(targetPosition.seconds)) } @@ -814,7 +909,7 @@ public final class MediaEditor { public func setAudioTrack(_ audioTrack: MediaAudioTrack?) { self.updateValues(mode: .skipRendering) { values in - return values.withUpdatedAudioTrack(audioTrack).withUpdatedAudioTrackSamples(nil).withUpdatedAudioTrackTrimRange(nil) + return values.withUpdatedAudioTrack(audioTrack).withUpdatedAudioTrackSamples(nil).withUpdatedAudioTrackTrimRange(nil).withUpdatedAudioTrackVolume(nil).withUpdatedAudioTrackStart(nil) } if let audioTrack { @@ -824,9 +919,19 @@ public final class MediaEditor { player.automaticallyWaitsToMinimizeStalling = false self.audioPlayer = player self.maybeGenerateAudioSamples(asset: audioAsset) + + self.setupTimeObservers() + + if !self.sourceIsVideo { + self.playerPromise.set(.single(player)) + } } else if let audioPlayer = self.audioPlayer { audioPlayer.pause() self.audioPlayer = nil + + if !self.sourceIsVideo { + self.playerPromise.set(.single(nil)) + } } } @@ -840,6 +945,20 @@ public final class MediaEditor { } } + public func setAudioTrackStart(_ start: Double?) { + self.updateValues(mode: .skipRendering) { values in + return values.withUpdatedAudioTrackStart(start) + } + } + + public func setAudioTrackVolume(_ volume: CGFloat?) { + self.updateValues(mode: .skipRendering) { values in + return values.withUpdatedAudioTrackVolume(volume) + } + + self.audioPlayer?.volume = Float(volume ?? 1.0) + } + private var previousUpdateTime: Double? private var scheduledUpdate = false private func updateRenderChain() { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index ed938a99e1..843d8358f1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -10,23 +10,27 @@ import AccountContext public struct MediaEditorResultPrivacy: Codable, Equatable { private enum CodingKeys: String, CodingKey { + case sendAsPeerId case privacy case timeout case disableForwarding case archive } + public let sendAsPeerId: EnginePeer.Id? public let privacy: EngineStoryPrivacy public let timeout: Int public let isForwardingDisabled: Bool public let pin: Bool public init( + sendAsPeerId: EnginePeer.Id?, privacy: EngineStoryPrivacy, timeout: Int, isForwardingDisabled: Bool, pin: Bool ) { + self.sendAsPeerId = sendAsPeerId self.privacy = privacy self.timeout = timeout self.isForwardingDisabled = isForwardingDisabled @@ -36,6 +40,7 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.sendAsPeerId = try container.decodeIfPresent(Int64.self, forKey: .sendAsPeerId).flatMap { EnginePeer.Id($0) } self.privacy = try container.decode(EngineStoryPrivacy.self, forKey: .privacy) self.timeout = Int(try container.decode(Int32.self, forKey: .timeout)) self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .disableForwarding) ?? false @@ -45,6 +50,7 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.sendAsPeerId?.toInt64(), forKey: .sendAsPeerId) try container.encode(self.privacy, forKey: .privacy) try container.encode(Int32(self.timeout), forKey: .timeout) try container.encode(self.isForwardingDisabled, forKey: .disableForwarding) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index ba3816dccb..f0d75d8d71 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -60,20 +60,24 @@ public struct MediaAudioTrack: Codable, Equatable { case path case artist case title + case duration } public let path: String public let artist: String? public let title: String? + public let duration: Double public init( path: String, artist: String?, - title: String? + title: String?, + duration: Double ) { self.path = path self.artist = artist self.title = title + self.duration = duration } } @@ -220,6 +224,8 @@ public final class MediaEditorValues: Codable, Equatable { case audioTrack case audioTrackTrimRange + case audioTrackStart + case audioTrackVolume } public let originalDimensions: PixelDimensions @@ -248,6 +254,8 @@ public final class MediaEditorValues: Codable, Equatable { public let audioTrack: MediaAudioTrack? public let audioTrackTrimRange: Range? + public let audioTrackStart: Double? + public let audioTrackVolume: CGFloat? public let audioTrackSamples: MediaAudioTrackSamples? init( @@ -272,6 +280,8 @@ public final class MediaEditorValues: Codable, Equatable { toolValues: [EditorToolKey: Any], audioTrack: MediaAudioTrack?, audioTrackTrimRange: Range?, + audioTrackStart: Double?, + audioTrackVolume: CGFloat?, audioTrackSamples: MediaAudioTrackSamples? ) { self.originalDimensions = originalDimensions @@ -295,6 +305,8 @@ public final class MediaEditorValues: Codable, Equatable { self.toolValues = toolValues self.audioTrack = audioTrack self.audioTrackTrimRange = audioTrackTrimRange + self.audioTrackStart = audioTrackStart + self.audioTrackVolume = audioTrackVolume self.audioTrackSamples = audioTrackSamples } @@ -346,6 +358,8 @@ public final class MediaEditorValues: Codable, Equatable { self.audioTrack = try container.decodeIfPresent(MediaAudioTrack.self, forKey: .audioTrack) self.audioTrackTrimRange = try container.decodeIfPresent(Range.self, forKey: .audioTrackTrimRange) + self.audioTrackStart = try container.decodeIfPresent(Double.self, forKey: .audioTrackStart) + self.audioTrackVolume = try container.decodeIfPresent(CGFloat.self, forKey: .audioTrackVolume) self.audioTrackSamples = nil } @@ -393,63 +407,73 @@ public final class MediaEditorValues: Codable, Equatable { try container.encodeIfPresent(self.audioTrack, forKey: .audioTrack) try container.encodeIfPresent(self.audioTrackTrimRange, forKey: .audioTrackTrimRange) + try container.encodeIfPresent(self.audioTrackStart, forKey: .audioTrackStart) + try container.encodeIfPresent(self.audioTrackVolume, forKey: .audioTrackVolume) } public func makeCopy() -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedVideoIsFullHd(_ videoIsFullHd: Bool) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedVideoIsMirrored(_ videoIsMirrored: Bool) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedAdditionalVideo(path: String, positionChanges: [VideoPositionChange]) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: path, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: path, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedAudioTrack(_ audioTrack: MediaAudioTrack?) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedAudioTrackTrimRange(_ audioTrackTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackSamples: self.audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) + } + + func withUpdatedAudioTrackStart(_ audioTrackStart: Double?) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples) + } + + func withUpdatedAudioTrackVolume(_ audioTrackVolume: CGFloat?) -> MediaEditorValues { + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples) } func withUpdatedAudioTrackSamples(_ audioTrackSamples: MediaAudioTrackSamples?) -> MediaEditorValues { - return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackSamples: audioTrackSamples) + return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackStart: self.audioTrackStart, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples) } public var resultDimensions: PixelDimensions { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index c649632d07..5b39d0811b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -397,7 +397,7 @@ public final class MediaEditorVideoExport { if let audioTrackRange = self.configuration.audioTimeRange { musicRange = audioTrackRange } - try? musicTrack.insertTimeRange(musicRange, of: musicAssetTrack, at: .zero) + try? musicTrack.insertTimeRange(musicRange, of: musicAssetTrack, at: CMTime(seconds: self.configuration.values.audioTrackStart ?? 0.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))) inputAsset = mixComposition } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 80c0d0b4e0..970b64d2f0 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -229,6 +229,7 @@ final class MediaEditorScreenComponent: Component { } var muteDidChange = false + var playbackDidChange = false } func makeState() -> State { @@ -921,12 +922,18 @@ final class MediaEditorScreenComponent: Component { let previousAudioData = self.appliedAudioData var audioData: VideoScrubberComponent.AudioData? if let audioTrack = mediaEditor?.values.audioTrack { + let trimRange = mediaEditor?.values.audioTrackTrimRange + let offset = mediaEditor?.values.audioTrackStart let audioSamples = mediaEditor?.values.audioTrackSamples audioData = VideoScrubberComponent.AudioData( artist: audioTrack.artist, title: audioTrack.title, samples: audioSamples?.samples, - peak: audioSamples?.peak ?? 0 + peak: audioSamples?.peak ?? 0, + duration: audioTrack.duration, + start: trimRange?.lowerBound, + end: trimRange?.upperBound, + offset: offset ?? 0.0 ) } self.appliedAudioData = audioData @@ -1273,16 +1280,30 @@ final class MediaEditorScreenComponent: Component { if (audioData == nil) != (previousAudioData == nil) { bottomControlsTransition = .easeInOut(duration: 0.25) } + + let minDuration: Double + let maxDuration: Double + if let mediaEditor, !mediaEditor.sourceIsVideo { + minDuration = 5.0 + maxDuration = 15.0 + } else { + minDuration = 1.0 + maxDuration = storyMaxVideoDuration + } + + let isAudioOnly = mediaEditor?.sourceIsVideo == false let scrubberSize = self.scrubber.update( transition: transition, component: AnyComponent(VideoScrubberComponent( context: component.context, generationTimestamp: playerState.generationTimestamp, + audioOnly: isAudioOnly, duration: playerState.duration, startPosition: playerState.timeRange?.lowerBound ?? 0.0, endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration), position: playerState.position, - maxDuration: storyMaxVideoDuration, + minDuration: minDuration, + maxDuration: maxDuration, isPlaying: playerState.isPlaying, frames: playerState.frames, framesUpdateTimestamp: playerState.framesUpdateTimestamp, @@ -1305,8 +1326,8 @@ final class MediaEditorScreenComponent: Component { audioTrimUpdated: { [weak mediaEditor] start, end, _, done in if let mediaEditor { mediaEditor.setAudioTrackTrimRange(start.. - if component.hasAppeared && !"".isEmpty { + if component.hasAppeared { playbackContentComponent = AnyComponentWithIdentity( id: "animatedIcon", component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( - name: "anim_storymute", - mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), - range: "".isEmpty ? (0.0, 0.5) : (0.5, 1.0) + name: "anim_storyplayback", + mode: state.playbackDidChange ? .animating(loop: false) : .still(position: .end), // : .still(position: .begin), + range: playerState.isPlaying ? (0.5, 1.0) : (0.0, 0.5) ), colors: ["__allcolors__": .white], size: CGSize(width: 30.0, height: 30.0) - ).tagged(muteButtonTag) + ).tagged(playbackButtonTag) ) ) } else { @@ -1526,11 +1558,10 @@ final class MediaEditorScreenComponent: Component { transition: transition, component: AnyComponent(CameraButton( content: playbackContentComponent, - action: { [weak mediaEditor] in + action: { [weak mediaEditor, weak state] in if let mediaEditor { -// state?.muteDidChange = true + state?.playbackDidChange = true mediaEditor.togglePlayback() -// state?.updated() } } )), @@ -1557,6 +1588,13 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } + } else { + if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { + playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in + playbackButtonView?.removeFromSuperview() + }) + playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } } let textCancelButtonSize = self.textCancelButton.update( @@ -1706,6 +1744,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate struct State { var privacy: MediaEditorResultPrivacy = MediaEditorResultPrivacy( + sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, isForwardingDisabled: false, @@ -2990,7 +3029,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func presentAudioPicker() { self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3"], forceDarkTheme: true, completion: { [weak self] urls in - guard let self, !urls.isEmpty, let url = urls.first else { + guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else { return } @@ -3006,7 +3045,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate title = data.stringValue } } - self.mediaEditor?.setAudioTrack(MediaAudioTrack(path: path, artist: artist, title: title)) + + let duration = audioAsset.duration.seconds + mediaEditor.setAudioTrack(MediaAudioTrack(path: path, artist: artist, title: title, duration: duration)) + if !mediaEditor.sourceIsVideo { + mediaEditor.setAudioTrackTrimRange(0 ..< min(15, duration), apply: true) + } + self.requestUpdate(transition: .easeInOut(duration: 0.2)) Queue.mainQueue().after(0.1) { @@ -3017,8 +3062,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func presentAudioOptions(sourceView: UIView) { let items: [ContextMenuItem] = [ - .custom(VolumeSliderContextItem(minValue: 0.0, value: 0.75, valueChanged: { _, _ in - + .custom(VolumeSliderContextItem(minValue: 0.0, value: 0.75, valueChanged: { [weak self] value, _ in + if let self { + self.mediaEditor?.setAudioTrackVolume(value) + } }), false), .action( ContextMenuActionItem( @@ -3567,6 +3614,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public var dismissed: () -> Void = { } public var willDismiss: () -> Void = { } + private var adminedChannels = Promise<[EnginePeer]>() private var closeFriends = Promise<[EnginePeer]>() private let storiesBlockedPeers: BlockedPeersContext @@ -3612,6 +3660,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if isEditing { if let initialPrivacy { self.state.privacy = MediaEditorResultPrivacy( + sendAsPeerId: nil, privacy: initialPrivacy, timeout: 86400, isForwardingDisabled: false, @@ -3626,7 +3675,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate ).start(next: { [weak self] state, peer in if let self, var privacy = state?.privacy { if case let .user(user) = peer, !user.isPremium && privacy.timeout != 86400 { - privacy = MediaEditorResultPrivacy(privacy: privacy.privacy, timeout: 86400, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin) + privacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: privacy.privacy, timeout: 86400, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin) + } else { + privacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: privacy.privacy, timeout: privacy.timeout, isForwardingDisabled: privacy.isForwardingDisabled, pin: privacy.pin) } self.state.privacy = privacy } @@ -3654,10 +3705,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.displayNode.view.addInteraction(dropInteraction) Queue.mainQueue().after(1.0) { + self.adminedChannels.set(.single([]) |> then(self.context.engine.peers.adminedPublicChannels(scope: .all))) self.closeFriends.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.CloseFriends())) } } - + func openPrivacySettings(_ privacy: MediaEditorResultPrivacy? = nil, completion: @escaping () -> Void = {}) { self.node.mediaEditor?.stop() @@ -3674,12 +3726,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate editing: false, initialPeerIds: Set(privacy.privacy.additionallyIncludePeers), closeFriends: self.closeFriends.get(), + adminedChannels: self.adminedChannels.get(), blockedPeersContext: self.storiesBlockedPeers ) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } + let sendAsPeerId = privacy.sendAsPeerId let initialPrivacy = privacy.privacy let timeout = privacy.timeout @@ -3691,11 +3745,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate timeout: privacy.timeout, mentions: mentions, stateContext: stateContext, - completion: { [weak self] privacy, allowScreenshots, pin, _, completed in + completion: { [weak self] sendAsPeerId, privacy, allowScreenshots, pin, _, completed in guard let self else { return } - self.state.privacy = MediaEditorResultPrivacy(privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, pin: pin) + self.state.privacy = MediaEditorResultPrivacy( + sendAsPeerId: sendAsPeerId, + privacy: privacy, + timeout: timeout, + isForwardingDisabled: !allowScreenshots, + pin: pin + ) if completed { completion() } @@ -3709,6 +3769,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } self.openPrivacySettings(MediaEditorResultPrivacy( + sendAsPeerId: sendAsPeerId, privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, @@ -3725,6 +3786,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } self.openPrivacySettings(MediaEditorResultPrivacy( + sendAsPeerId: sendAsPeerId, privacy: privacy, timeout: timeout, isForwardingDisabled: !allowScreenshots, @@ -3766,7 +3828,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate allowScreenshots: !isForwardingDisabled, pin: pin, stateContext: stateContext, - completion: { [weak self] result, isForwardingDisabled, pin, peers, completed in + completion: { [weak self] _, result, isForwardingDisabled, pin, peers, completed in guard let self, completed else { return } @@ -3800,7 +3862,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate guard let self else { return } - self.state.privacy = MediaEditorResultPrivacy(privacy: self.state.privacy.privacy, timeout: timeout ?? 86400, isForwardingDisabled: self.state.privacy.isForwardingDisabled, pin: self.state.privacy.pin) + self.state.privacy = MediaEditorResultPrivacy( + sendAsPeerId: self.state.privacy.sendAsPeerId, + privacy: self.state.privacy.privacy, + timeout: timeout ?? 86400, + isForwardingDisabled: self.state.privacy.isForwardingDisabled, + pin: self.state.privacy.pin + ) } let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index 3eb9c6dd5f..79a2e2ac22 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -16,7 +16,6 @@ private let scrubberHeight: CGFloat = 39.0 private let collapsedScrubberHeight: CGFloat = 26.0 private let borderHeight: CGFloat = 1.0 + UIScreenPixel private let frameWidth: CGFloat = 24.0 -private let minumumDuration: CGFloat = 1.0 private class VideoFrameLayer: SimpleShapeLayer { private let stripeLayer = SimpleShapeLayer() @@ -48,14 +47,20 @@ final class VideoScrubberComponent: Component { let title: String? let samples: Data? let peak: Int32 + let duration: Double + let start: Double? + let end: Double? + let offset: Double? } let context: AccountContext let generationTimestamp: Double + let audioOnly: Bool let duration: Double let startPosition: Double let endPosition: Double let position: Double + let minDuration: Double let maxDuration: Double let isPlaying: Bool let frames: [UIImage] @@ -69,10 +74,12 @@ final class VideoScrubberComponent: Component { init( context: AccountContext, generationTimestamp: Double, + audioOnly: Bool, duration: Double, startPosition: Double, endPosition: Double, position: Double, + minDuration: Double, maxDuration: Double, isPlaying: Bool, frames: [UIImage], @@ -85,10 +92,12 @@ final class VideoScrubberComponent: Component { ) { self.context = context self.generationTimestamp = generationTimestamp + self.audioOnly = audioOnly self.duration = duration self.startPosition = startPosition self.endPosition = endPosition self.position = position + self.minDuration = minDuration self.maxDuration = maxDuration self.isPlaying = isPlaying self.frames = frames @@ -107,6 +116,9 @@ final class VideoScrubberComponent: Component { if lhs.generationTimestamp != rhs.generationTimestamp { return false } + if lhs.audioOnly != rhs.audioOnly { + return false + } if lhs.duration != rhs.duration { return false } @@ -119,6 +131,9 @@ final class VideoScrubberComponent: Component { if lhs.position != rhs.position { return false } + if lhs.minDuration != rhs.minDuration { + return false + } if lhs.maxDuration != rhs.maxDuration { return false } @@ -140,15 +155,16 @@ final class VideoScrubberComponent: Component { private let audioBackgroundView: BlurredBackgroundView private let audioVibrancyView: UIVisualEffectView private let audioVibrancyContainer: UIView - private let audioTrimView = TrimView(frame: .zero) private let audioButton = UIButton() + private let audioContentContainerView: UIView + private let audioContentMaskView: UIImageView private let audioIconView: UIImageView private let audioTitle = ComponentView() private let audioWaveform = ComponentView() - private let videoTrimView = TrimView(frame: .zero) + private let trimView = TrimView(frame: .zero) private let cursorView = HandleView() private let transparentFramesContainer = UIView() @@ -187,6 +203,12 @@ final class VideoScrubberComponent: Component { self.audioVibrancyContainer = UIView() self.audioVibrancyView.contentView.addSubview(self.audioVibrancyContainer) + self.audioContentContainerView = UIView() + self.audioContentContainerView.clipsToBounds = true + + self.audioContentMaskView = UIImageView() + self.audioContentContainerView.mask = self.audioContentMaskView + self.audioIconView = UIImageView(image: UIImage(bundleImageName: "Media Editor/SmallAudio")) self.audioButton.isUserInteractionEnabled = false @@ -194,6 +216,8 @@ final class VideoScrubberComponent: Component { super.init(frame: frame) + self.clipsToBounds = false + self.disablesInteractiveModalDismiss = true self.disablesInteractiveKeyboardGestureRecognizer = true @@ -222,14 +246,12 @@ final class VideoScrubberComponent: Component { self.audioClippingView.addSubview(self.audioContainerView) self.audioContainerView.addSubview(self.audioBackgroundView) self.audioBackgroundView.addSubview(self.audioVibrancyView) - - self.addSubview(self.audioTrimView) - + self.addSubview(self.audioIconView) self.addSubview(self.transparentFramesContainer) self.addSubview(self.opaqueFramesContainer) - self.addSubview(self.videoTrimView) + self.addSubview(self.trimView) self.addSubview(self.audioButton) self.addSubview(self.videoButton) @@ -242,13 +264,17 @@ final class VideoScrubberComponent: Component { } self.displayLink?.isPaused = true - self.videoTrimView.updated = { [weak self] transition in + self.trimView.updated = { [weak self] transition in self?.state?.updated(transition: transition) } - self.videoTrimView.trimUpdated = { [weak self] startValue, endValue, updatedEnd, done in - if let component = self?.component { - component.videoTrimUpdated(startValue, endValue, updatedEnd, done) + self.trimView.trimUpdated = { [weak self] startValue, endValue, updatedEnd, done in + if let self, let component = self.component { + if self.isAudioSelected || component.audioOnly { + component.audioTrimUpdated(startValue, endValue, updatedEnd, done) + } else { + component.videoTrimUpdated(startValue, endValue, updatedEnd, done) + } } } @@ -258,6 +284,19 @@ final class VideoScrubberComponent: Component { let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressed(_:))) longPressGesture.delegate = self self.addGestureRecognizer(longPressGesture) + + let maskImage = generateImage(CGSize(width: 100.0, height: 50.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.75, 0.95, 1.0] + let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + })?.stretchableImage(withLeftCapWidth: 40, topCapHeight: 0) + self.audioContentMaskView.image = maskImage } required init?(coder: NSCoder) { @@ -280,7 +319,7 @@ final class VideoScrubberComponent: Component { guard let component = self.component, component.audioData != nil, case .began = gestureRecognizer.state else { return } - component.audioLongPressed?(self.audioContainerView) + component.audioLongPressed?(self.audioClippingView) } @objc private func audioButtonPressed() { @@ -323,8 +362,8 @@ final class VideoScrubberComponent: Component { let cursorPositionFraction = duration > 0.0 ? position / duration : 0.0 let cursorPosition = floorToScreenPixels(handleWidth + handleWidth / 2.0 - cursorPadding + (size.width - handleWidth * 3.0 + cursorPadding * 2.0) * cursorPositionFraction) var cursorFrame = CGRect(origin: CGPoint(x: cursorPosition - handleWidth / 2.0, y: -5.0 - UIScreenPixel), size: CGSize(width: handleWidth, height: height)) - cursorFrame.origin.x = max(self.videoTrimView.leftHandleView.frame.maxX - cursorPadding, cursorFrame.origin.x) - cursorFrame.origin.x = min(self.videoTrimView.rightHandleView.frame.minX + cursorPadding, cursorFrame.origin.x) + cursorFrame.origin.x = max(self.trimView.leftHandleView.frame.maxX - cursorPadding, cursorFrame.origin.x) + cursorFrame.origin.x = min(self.trimView.rightHandleView.frame.minX + cursorPadding, cursorFrame.origin.x) return cursorFrame } @@ -360,7 +399,9 @@ final class VideoScrubberComponent: Component { if let previousComponent { if previousComponent.audioData == nil, component.audioData != nil { self.positionAnimation = nil - self.isAudioSelected = true + if !component.audioOnly { + self.isAudioSelected = true + } animateAudioAppearance = true } else if previousComponent.audioData != nil, component.audioData == nil { self.positionAnimation = nil @@ -384,40 +425,80 @@ final class VideoScrubberComponent: Component { videoTransition = .easeInOut(duration: 0.25) } + let totalWidth = scrubberSize.width - handleWidth + var audioTotalWidth = scrubberSize.width + var originY: CGFloat = 0 var totalHeight = scrubberSize.height var audioAlpha: CGFloat = 0.0 - if let _ = component.audioData { - totalHeight += collapsedScrubberHeight + scrubberSpacing - audioAlpha = 1.0 - - originY += self.isAudioSelected ? scrubberHeight : collapsedScrubberHeight - originY += scrubberSpacing - - if self.isAudioSelected { + if let audioData = component.audioData { + if component.audioOnly { audioScrubberHeight = scrubberHeight - videoScrubberHeight = collapsedScrubberHeight + audioAlpha = 1.0 + } else { + totalHeight += collapsedScrubberHeight + scrubberSpacing + audioAlpha = 1.0 + + originY += self.isAudioSelected ? scrubberHeight : collapsedScrubberHeight + originY += scrubberSpacing + + if self.isAudioSelected { + audioScrubberHeight = scrubberHeight + videoScrubberHeight = collapsedScrubberHeight + } + + if component.duration > 0.0 { + let audioFraction = audioData.duration / component.duration + audioTotalWidth = ceil(totalWidth * audioFraction) + } } } else { self.isAudioSelected = false } audioTransition.setAlpha(view: self.audioClippingView, alpha: audioAlpha) - self.audioButton.isUserInteractionEnabled = !self.isAudioSelected - self.videoButton.isUserInteractionEnabled = self.isAudioSelected + self.audioButton.isUserInteractionEnabled = component.audioData != nil && !component.audioOnly && !self.isAudioSelected + self.videoButton.isUserInteractionEnabled = component.audioData != nil && !component.audioOnly && self.isAudioSelected - let audioClippingFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: audioScrubberHeight)) + var audioClipOrigin: CGFloat = 0.0 + var audioClipWidth = availableSize.width + 18.0 + if !self.isAudioSelected { + if let audioData = component.audioData, !component.audioOnly { + let duration: Double + if let end = audioData.end, let start = audioData.start, component.duration > 0.0 { + duration = end - start + } else { + duration = component.endPosition - component.startPosition + } + if component.duration > 0.0 { + let fraction = duration / component.duration + audioClipWidth = availableSize.width * fraction + + audioClipOrigin = (component.startPosition + (audioData.offset ?? 0.0)) / component.duration * availableSize.width + } + } else { + audioClipWidth = availableSize.width + } + } + + let audioClippingFrame = CGRect(origin: CGPoint(x: audioClipOrigin, y: 0.0), size: CGSize(width: audioClipWidth, height: audioScrubberHeight)) audioTransition.setFrame(view: self.audioButton, frame: audioClippingFrame) audioTransition.setFrame(view: self.audioClippingView, frame: audioClippingFrame) - let audioContainerFrame = CGRect(origin: .zero, size: audioClippingFrame.size) + audioTransition.setCornerRadius(layer: self.audioClippingView.layer, cornerRadius: self.isAudioSelected ? 0.0 : 9.0) + + let audioContainerFrame = CGRect(origin: .zero, size: CGSize(width: audioTotalWidth, height: audioScrubberHeight)) audioTransition.setFrame(view: self.audioContainerView, frame: audioContainerFrame) - audioTransition.setFrame(view: self.audioBackgroundView, frame: CGRect(origin: .zero, size: audioClippingFrame.size)) - self.audioBackgroundView.update(size: audioClippingFrame.size, transition: audioTransition.containedViewLayoutTransition) - audioTransition.setFrame(view: self.audioVibrancyView, frame: CGRect(origin: .zero, size: audioClippingFrame.size)) - audioTransition.setFrame(view: self.audioVibrancyContainer, frame: CGRect(origin: .zero, size: audioClippingFrame.size)) + audioTransition.setFrame(view: self.audioBackgroundView, frame: CGRect(origin: .zero, size: audioContainerFrame.size)) + self.audioBackgroundView.update(size: audioContainerFrame.size, transition: audioTransition.containedViewLayoutTransition) + audioTransition.setFrame(view: self.audioVibrancyView, frame: CGRect(origin: .zero, size: audioContainerFrame.size)) + audioTransition.setFrame(view: self.audioVibrancyContainer, frame: CGRect(origin: .zero, size: audioContainerFrame.size)) - if let audioData = component.audioData { + let containerFrame = CGRect(origin: .zero, size: CGSize(width: audioClipWidth, height: audioContainerFrame.height)) + audioTransition.setFrame(view: self.audioContentContainerView, frame: containerFrame) + audioTransition.setFrame(view: self.audioContentMaskView, frame: CGRect(origin: .zero, size: containerFrame.size)) + + if let audioData = component.audioData, !component.audioOnly { var components: [String] = [] if let artist = audioData.artist { components.append(artist) @@ -446,7 +527,7 @@ final class VideoScrubberComponent: Component { audioTransition.setAlpha(view: self.audioIconView, alpha: self.isAudioSelected ? 0.0 : 1.0) - let audioIconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0), y: floorToScreenPixels((audioScrubberHeight - iconSize.height) / 2.0)), size: iconSize) + let audioIconFrame = CGRect(origin: CGPoint(x: max(8.0, floorToScreenPixels((audioClipWidth - totalWidth) / 2.0)), y: floorToScreenPixels((audioScrubberHeight - iconSize.height) / 2.0)), size: iconSize) audioTransition.setBounds(view: self.audioIconView, bounds: CGRect(origin: .zero, size: audioIconFrame.size)) audioTransition.setPosition(view: self.audioIconView, position: audioIconFrame.center) @@ -454,12 +535,13 @@ final class VideoScrubberComponent: Component { if view.superview == nil { view.alpha = 0.0 view.isUserInteractionEnabled = false - self.audioContainerView.addSubview(self.audioIconView) - self.audioContainerView.addSubview(view) + self.audioContainerView.addSubview(self.audioContentContainerView) + self.audioContentContainerView.addSubview(self.audioIconView) + self.audioContentContainerView.addSubview(view) } audioTransition.setAlpha(view: view, alpha: self.isAudioSelected ? 0.0 : 1.0) - let audioTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) + iconSize.width + spacing, y: floorToScreenPixels((audioScrubberHeight - audioTitleSize.height) / 2.0)), size: audioTitleSize) + let audioTitleFrame = CGRect(origin: CGPoint(x: audioIconFrame.maxX + spacing, y: floorToScreenPixels((audioScrubberHeight - audioTitleSize.height) / 2.0)), size: audioTitleSize) audioTransition.setBounds(view: view, bounds: CGRect(origin: .zero, size: audioTitleFrame.size)) audioTransition.setPosition(view: view, position: audioTitleFrame.center) } @@ -487,7 +569,7 @@ final class VideoScrubberComponent: Component { ) ), environment: {}, - containerSize: CGSize(width: audioContainerFrame.width * 5.0, height: scrubberHeight) + containerSize: CGSize(width: audioContainerFrame.width, height: scrubberHeight) ) if let view = self.audioWaveform.view { if view.superview == nil { @@ -496,26 +578,11 @@ final class VideoScrubberComponent: Component { view.layer.animateScaleY(from: 0.01, to: 1.0, duration: 0.2) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - audioTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isAudioSelected ? 0.0 : 6.0), size: audioWaveformSize)) + audioTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isAudioSelected || component.audioOnly ? 0.0 : 6.0), size: audioWaveformSize)) } } let bounds = CGRect(origin: .zero, size: scrubberSize) - let totalWidth = scrubberSize.width - handleWidth - - audioTransition.setAlpha(view: self.audioTrimView, alpha: self.isAudioSelected ? 1.0 : 0.0) - audioTransition.setFrame(view: self.audioTrimView, frame: bounds) - - let _ = self.audioTrimView.update( - totalWidth: totalWidth, - scrubberSize: scrubberSize, - duration: component.duration, - startPosition: component.startPosition, - endPosition: component.duration, - position: component.position, - maxDuration: component.maxDuration, - transition: transition - ) if component.framesUpdateTimestamp != previousFramesUpdateTimestamp { for i in 0 ..< component.frames.count { @@ -546,17 +613,36 @@ final class VideoScrubberComponent: Component { } } - let (leftHandleFrame, rightHandleFrame) = self.videoTrimView.update( + var startPosition = component.startPosition + var endPosition = component.endPosition + if self.isAudioSelected, let audioData = component.audioData { + if let start = audioData.start { + startPosition = start + } + if let end = audioData.end { + endPosition = end + } + } + + let (leftHandleFrame, rightHandleFrame) = self.trimView.update( totalWidth: totalWidth, scrubberSize: scrubberSize, duration: component.duration, - startPosition: component.startPosition, - endPosition: component.endPosition, + startPosition: startPosition, + endPosition: endPosition, position: component.position, + minDuration: component.minDuration, maxDuration: component.maxDuration, transition: transition ) + var containerLeftEdge = leftHandleFrame.maxX + var containerRightEdge = rightHandleFrame.minX + if self.isAudioSelected && component.duration > 0.0 { + containerLeftEdge = floorToScreenPixels(component.startPosition / component.duration * scrubberSize.width) + containerRightEdge = floorToScreenPixels(component.endPosition / component.duration * scrubberSize.width) + } + if self.isPanningPositionHandle || !component.isPlaying { self.positionAnimation = nil self.displayLink?.isPaused = true @@ -576,12 +662,13 @@ final class VideoScrubberComponent: Component { } // transition.setAlpha(view: self.cursorView, alpha: self.isPanningTrimHandle ? 0.0 : 1.0) - videoTransition.setAlpha(view: self.videoTrimView, alpha: self.isAudioSelected ? 0.0 : 1.0) - videoTransition.setFrame(view: self.videoTrimView, frame: bounds.offsetBy(dx: 0.0, dy: originY)) + videoTransition.setFrame(view: self.trimView, frame: bounds.offsetBy(dx: 0.0, dy: self.isAudioSelected ? 0.0 : originY)) let handleInset: CGFloat = 7.0 videoTransition.setFrame(view: self.transparentFramesContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: CGSize(width: scrubberSize.width, height: videoScrubberHeight))) - videoTransition.setFrame(view: self.opaqueFramesContainer, frame: CGRect(origin: CGPoint(x: leftHandleFrame.maxX - handleInset, y: originY), size: CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX + handleInset * 2.0, height: videoScrubberHeight))) - videoTransition.setBounds(view: self.opaqueFramesContainer, bounds: CGRect(origin: CGPoint(x: leftHandleFrame.maxX - handleInset, y: 0.0), size: CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX + handleInset * 2.0, height: videoScrubberHeight))) + videoTransition.setFrame(view: self.opaqueFramesContainer, frame: CGRect(origin: CGPoint(x: containerLeftEdge - handleInset, y: originY), size: CGSize(width: containerRightEdge - containerLeftEdge + handleInset * 2.0, height: videoScrubberHeight))) + videoTransition.setBounds(view: self.opaqueFramesContainer, bounds: CGRect(origin: CGPoint(x: containerLeftEdge - handleInset, y: 0.0), size: CGSize(width: containerRightEdge - containerLeftEdge + handleInset * 2.0, height: videoScrubberHeight))) + + videoTransition.setCornerRadius(layer: self.opaqueFramesContainer.layer, cornerRadius: self.isAudioSelected ? 9.0 : 0.0) videoTransition.setFrame(view: self.videoButton, frame: bounds.offsetBy(dx: 0.0, dy: originY)) @@ -673,7 +760,6 @@ private class TrimView: UIView { self.rightHandleView.tintColor = .white self.rightHandleView.hitTestSlop = UIEdgeInsets(top: -8.0, left: -9.0, bottom: -8.0, right: -9.0) - self.borderView.image = generateImage(CGSize(width: 1.0, height: scrubberHeight), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) @@ -744,8 +830,8 @@ private class TrimView: UIView { let fraction = (location.x - start) / length var startValue = max(0.0, params.duration * fraction) - if startValue > params.endPosition - minumumDuration { - startValue = max(0.0, params.endPosition - minumumDuration) + if startValue > params.endPosition - params.minDuration { + startValue = max(0.0, params.endPosition - params.minDuration) } var endValue = params.endPosition if endValue - startValue > params.maxDuration { @@ -782,8 +868,8 @@ private class TrimView: UIView { let fraction = (location.x - start) / length var endValue = min(params.duration, params.duration * fraction) - if endValue < params.startPosition + minumumDuration { - endValue = min(params.duration, params.startPosition + minumumDuration) + if endValue < params.startPosition + params.minDuration { + endValue = min(params.duration, params.startPosition + params.minDuration) } var startValue = params.startPosition if endValue - startValue > params.maxDuration { @@ -814,6 +900,7 @@ private class TrimView: UIView { startPosition: Double, endPosition: Double, position: Double, + minDuration: Double, maxDuration: Double )? @@ -824,11 +911,12 @@ private class TrimView: UIView { startPosition: Double, endPosition: Double, position: Double, + minDuration: Double, maxDuration: Double, transition: Transition ) -> (leftHandleFrame: CGRect, rightHandleFrame: CGRect) { - self.params = (duration, startPosition, endPosition, position, maxDuration) + self.params = (duration, startPosition, endPosition, position, minDuration, maxDuration) let trimColor = self.isPanningTrimHandle ? UIColor(rgb: 0xf8d74a) : .white transition.setTintColor(view: self.leftHandleView, color: trimColor) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 8d30606e17..fc8af9c569 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -32,33 +32,38 @@ final class ShareWithPeersScreenComponent: Component { let context: AccountContext let stateContext: ShareWithPeersScreen.StateContext let initialPrivacy: EngineStoryPrivacy + let initialSendAsPeerId: EnginePeer.Id? let screenshot: Bool let pin: Bool let timeout: Int let mentions: [String] let categoryItems: [CategoryItem] let optionItems: [OptionItem] - let completion: (EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void + let completion: (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void let editBlockedPeers: (EngineStoryPrivacy, Bool, Bool) -> Void + let peerCompletion: (EnginePeer.Id) -> Void init( context: AccountContext, stateContext: ShareWithPeersScreen.StateContext, initialPrivacy: EngineStoryPrivacy, + initialSendAsPeerId: EnginePeer.Id?, screenshot: Bool, pin: Bool, timeout: Int, mentions: [String], categoryItems: [CategoryItem], optionItems: [OptionItem], - completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, + completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, - editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void + editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + peerCompletion: @escaping (EnginePeer.Id) -> Void ) { self.context = context self.stateContext = stateContext self.initialPrivacy = initialPrivacy + self.initialSendAsPeerId = initialSendAsPeerId self.screenshot = screenshot self.pin = pin self.timeout = timeout @@ -68,6 +73,7 @@ final class ShareWithPeersScreenComponent: Component { self.completion = completion self.editCategory = editCategory self.editBlockedPeers = editBlockedPeers + self.peerCompletion = peerCompletion } static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool { @@ -80,6 +86,9 @@ final class ShareWithPeersScreenComponent: Component { if lhs.initialPrivacy != rhs.initialPrivacy { return false } + if lhs.initialSendAsPeerId != rhs.initialSendAsPeerId { + return false + } if lhs.screenshot != rhs.screenshot { return false } @@ -312,6 +321,8 @@ final class ShareWithPeersScreenComponent: Component { private var ignoreScrolling: Bool = false private var isDismissed: Bool = false + private var sendAsPeerId: EnginePeer.Id? + private var selectedPeers: [EnginePeer.Id] = [] private var selectedGroups: [EnginePeer.Id] = [] private var groupPeersMap: [EnginePeer.Id: [EnginePeer.Id]] = [:] @@ -750,6 +761,40 @@ final class ShareWithPeersScreenComponent: Component { self.selectedGroups.removeAll(where: { unselectedGroupIds.contains($0) }) } + private func presentSendAsPeer() { + guard let component = self.component else { + return + } + + let stateContext = ShareWithPeersScreen.StateContext( + context: component.context, + subject: .peers(peers: self.effectiveStateValue?.sendAsPeers ?? [], peerId: self.sendAsPeerId), + editing: false + ) + + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + let controller = ShareWithPeersScreen( + context: component.context, + initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), + stateContext: stateContext, + completion: { _, _, _, _, _, _ in }, + editCategory: { _, _, _ in }, + editBlockedPeers: { _, _, _ in }, + peerCompletion: { [weak self] peerId in + guard let self else { + return + } + self.sendAsPeerId = peerId + self.state?.updated(transition: .spring(duration: 0.4)) + } + ) + self.environment?.controller()?.push(controller) + }) + } + private func updateScrolling(transition: Transition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return @@ -789,9 +834,22 @@ final class ShareWithPeersScreenComponent: Component { var validSectionHeaders: [AnyHashable] = [] var validSectionBackgrounds: [AnyHashable] = [] var sectionOffset: CGFloat = itemLayout.navigationHeight + + var hasCategories = false + if case .stories = component.stateContext.subject { + if let peerId = self.sendAsPeerId, peerId.isGroupOrChannel { + } else { + hasCategories = true + } + } + for sectionIndex in 0 ..< itemLayout.sections.count { let section = itemLayout.sections[sectionIndex] - + + if section.id == 2 && !hasCategories { + continue + } + if case .blocks = itemLayout.style { let sectionBackgroundFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top), size: CGSize(width: itemLayout.containerSize.width, height: section.totalHeight - section.insets.top)) @@ -823,12 +881,15 @@ final class ShareWithPeersScreenComponent: Component { var minSectionHeader: UIView? do { var sectionHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) - - let sectionHeaderMinY = topOffset + itemLayout.containerInset + itemLayout.navigationHeight - let sectionHeaderMaxY = itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset + section.totalHeight - 28.0 - - sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY) - sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY) + if case .stories = component.stateContext.subject { + sectionHeaderFrame = CGRect(origin: CGPoint(x: sectionHeaderFrame.minX, y: sectionOffset), size: sectionHeaderFrame.size) + } else { + let sectionHeaderMinY = topOffset + itemLayout.containerInset + itemLayout.navigationHeight + let sectionHeaderMaxY = itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset + section.totalHeight - 28.0 + + sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY) + sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY) + } if visibleFrame.intersects(sectionHeaderFrame) { validSectionHeaders.append(section.id) @@ -845,7 +906,9 @@ final class ShareWithPeersScreenComponent: Component { } let sectionTitle: String - if section.id == 0 { + if section.id == 0, case .stories = component.stateContext.subject { + sectionTitle = "POST STORY AS" + } else if section.id == 2 { sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader } else if section.id == 1 { sectionTitle = environment.strings.Story_Privacy_ContactsHeader @@ -874,7 +937,15 @@ final class ShareWithPeersScreenComponent: Component { ) if let sectionHeaderView = sectionHeader.view { if sectionHeaderView.superview == nil { - self.scrollContentClippingView.addSubview(sectionHeaderView) + if case .stories = component.stateContext.subject { + self.itemContainerView.addSubview(sectionHeaderView) + } else { + self.scrollContentClippingView.addSubview(sectionHeaderView) + } + + if !transition.animation.isImmediate { + sectionHeaderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } if minSectionHeader == nil { minSectionHeader = sectionHeaderView @@ -885,6 +956,98 @@ final class ShareWithPeersScreenComponent: Component { } if section.id == 0 { + var peers = stateValue.sendAsPeers + if case .stories = component.stateContext.subject { + if let peerId = self.sendAsPeerId, let selectedPeer = stateValue.sendAsPeers.first(where: { $0.id == peerId }) { + peers = [selectedPeer] + } else if let peer = peers.first { + peers = [peer] + } + } + for i in 0 ..< peers.count { + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + let peer = peers[i] + let itemId = AnyHashable(peer.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + let subtitle: String? + if case .user = peer { + subtitle = "personal account" + } else { + subtitle = "channel" + } + + var isStories = false + var accessory: PeerListItemComponent.RightAccessory + if case .stories = component.stateContext.subject { + accessory = .disclosure + isStories = true + } else { + if let selectedPeerId = self.sendAsPeerId { + accessory = selectedPeerId == peer.id ? .check : .none + } else { + accessory = component.context.account.peerId == peer.id ? .check : .none + } + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: itemLayout.sideInset, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + subtitle: subtitle, + subtitleAccessory: .none, + presence: nil, + rightAccessory: accessory, + selectionState: .none, + hasNext: i < peers.count - 1, + action: { [weak self] peer in + guard let self else { + return + } + if isStories { + let _ = self.presentSendAsPeer() + } else { + self.environment?.controller()?.dismiss() + self.component?.peerCompletion(peer.id) + } + } + )), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + if hasCategories { + sectionOffset += 24.0 + } + } else if section.id == 2 { for i in 0 ..< component.categoryItems.count { let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) if !visibleBounds.intersects(itemFrame) { @@ -998,6 +1161,10 @@ final class ShareWithPeersScreenComponent: Component { } else { self.itemContainerView.addSubview(itemView) } + + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } itemTransition.setFrame(view: itemView, frame: itemFrame) } @@ -1171,7 +1338,7 @@ final class ShareWithPeersScreenComponent: Component { itemTransition.setFrame(view: itemView, frame: itemFrame) } } - } else if section.id == 2 && section.itemCount > 0 { + } else if section.id == 3 && section.itemCount > 0 { for i in 0 ..< component.optionItems.count { let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) if !visibleBounds.intersects(itemFrame) { @@ -1244,7 +1411,12 @@ final class ShareWithPeersScreenComponent: Component { } let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600)) - let footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string + var footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string + + if self.sendAsPeerId?.isGroupOrChannel == true { + footerText = "Keep this story on channel profile even after it expires in 24 hours." + } + let footerSize = sectionFooter.update( transition: sectionFooterTransition, component: AnyComponent(MultilineTextComponent( @@ -1272,7 +1444,13 @@ final class ShareWithPeersScreenComponent: Component { if !validIds.contains(id) { removeIds.append(id) if let itemView = item.view { - itemView.removeFromSuperview() + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } } } } @@ -1285,7 +1463,13 @@ final class ShareWithPeersScreenComponent: Component { if !validSectionHeaders.contains(id) { removeSectionHeaderIds.append(id) if let itemView = item.view { - itemView.removeFromSuperview() + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } } } } @@ -1294,10 +1478,31 @@ final class ShareWithPeersScreenComponent: Component { } var removeSectionBackgroundIds: [Int] = [] + var removeSectionFooterIds: [Int] = [] for (id, item) in self.visibleSectionBackgrounds { if !validSectionBackgrounds.contains(id) { removeSectionBackgroundIds.append(id) - item.removeFromSuperview() + if !transition.animation.isImmediate { + item.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + item.removeFromSuperview() + }) + } else { + item.removeFromSuperview() + } + } + } + for (id, item) in self.visibleSectionFooters { + if !validSectionBackgrounds.contains(id) { + removeSectionFooterIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } } } for id in removeSectionBackgroundIds { @@ -1440,6 +1645,19 @@ final class ShareWithPeersScreenComponent: Component { } } + private var hasBlocksStyle: Bool { + guard let component = self.component else { + return false + } + if case .stories = component.stateContext.subject { + return true + } else if case .peers = component.stateContext.subject { + return true + } else { + return false + } + } + func update(component: ShareWithPeersScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { guard !self.isDismissed else { return availableSize @@ -1459,8 +1677,11 @@ final class ShareWithPeersScreenComponent: Component { var sideInset: CGFloat = 0.0 if case .stories = component.stateContext.subject { sideInset = 16.0 - self.scrollView.bounces = false + self.scrollView.isScrollEnabled = false self.dismissPanGesture?.isEnabled = true + } else if case .peers = component.stateContext.subject { + sideInset = 16.0 + self.dismissPanGesture?.isEnabled = false } else { self.scrollView.bounces = true self.dismissPanGesture?.isEnabled = false @@ -1475,6 +1696,8 @@ final class ShareWithPeersScreenComponent: Component { let containerSideInset = floorToScreenPixels((availableSize.width - containerWidth) / 2.0) if self.component == nil { + self.sendAsPeerId = component.initialSendAsPeerId + switch component.initialPrivacy.base { case .everyone: self.selectedCategories.insert(.everyone) @@ -1496,6 +1719,9 @@ final class ShareWithPeersScreenComponent: Component { var applyState = false self.defaultStateValue = component.stateContext.stateValue self.selectedPeers = Array(component.stateContext.initialPeerIds) + if case let .peers(_, peerId) = component.stateContext.subject { + self.sendAsPeerId = peerId + } self.stateDisposable = (component.stateContext.state |> deliverOnMainQueue).start(next: { [weak self] stateValue in @@ -1522,7 +1748,7 @@ final class ShareWithPeersScreenComponent: Component { self.backgroundView.image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - if case .stories = component.stateContext.subject { + if self.hasBlocksStyle { context.setFillColor(environment.theme.list.modalBlocksBackgroundColor.cgColor) } else { context.setFillColor(environment.theme.list.plainBackgroundColor.cgColor) @@ -1531,7 +1757,7 @@ final class ShareWithPeersScreenComponent: Component { context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height * 0.5))) })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 19) - if case .stories = component.stateContext.subject { + if self.hasBlocksStyle { self.navigationBackgroundView.updateColor(color: environment.theme.list.modalBlocksBackgroundColor, transition: .immediate) self.navigationSeparatorLayer.backgroundColor = UIColor.clear.cgColor self.bottomBackgroundView.updateColor(color: environment.theme.list.modalBlocksBackgroundColor, transition: .immediate) @@ -1549,7 +1775,7 @@ final class ShareWithPeersScreenComponent: Component { let itemLayoutStyle: ShareWithPeersScreenComponent.Style let itemsContainerWidth: CGFloat let navigationTextFieldSize: CGSize - if case .stories = component.stateContext.subject { + if self.hasBlocksStyle { itemLayoutStyle = .blocks itemsContainerWidth = containerWidth - sideInset * 2.0 navigationTextFieldSize = .zero @@ -1705,45 +1931,55 @@ final class ShareWithPeersScreenComponent: Component { containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) ) + var hasCategories = false + if case .stories = component.stateContext.subject { + if let peerId = self.sendAsPeerId, peerId.isGroupOrChannel { + } else { + hasCategories = true + } + } + var footersTotalHeight: CGFloat = 0.0 if case let .stories(editing) = component.stateContext.subject { let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor) - let firstFooterText: String - if let grayListPeers = component.stateContext.stateValue?.grayListPeers, !grayListPeers.isEmpty { - let footerValue = environment.strings.Story_Privacy_GrayListPeople(Int32(grayListPeers.count)) - firstFooterText = environment.strings.Story_Privacy_GrayListSelected(footerValue).string - } else { - firstFooterText = environment.strings.Story_Privacy_GrayListSelect - } - let footerInset: CGFloat = 7.0 - let firstFooterSize = self.footerTemplateItem.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .markdown(text: firstFooterText, attributes: MarkdownAttributes( - body: body, - bold: bold, - link: link, - linkAttribute: { url in - return ("URL", url) + if hasCategories { + let firstFooterText: String + if let grayListPeers = component.stateContext.stateValue?.grayListPeers, !grayListPeers.isEmpty { + let footerValue = environment.strings.Story_Privacy_GrayListPeople(Int32(grayListPeers.count)) + firstFooterText = environment.strings.Story_Privacy_GrayListSelected(footerValue).string + } else { + firstFooterText = environment.strings.Story_Privacy_GrayListSelect + } + + let firstFooterSize = self.footerTemplateItem.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: firstFooterText, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: link, + linkAttribute: { url in + return ("URL", url) + } + )), + maximumNumberOfLines: 0, + lineSpacing: 0.1, + highlightColor: .clear, + highlightAction: { _ in + return nil + }, + tapAction: { _, _ in } )), - maximumNumberOfLines: 0, - lineSpacing: 0.1, - highlightColor: .clear, - highlightAction: { _ in - return nil - }, - tapAction: { _, _ in - } - )), - environment: {}, - containerSize: CGSize(width: itemsContainerWidth - 16.0 * 2.0, height: 1000.0) - ) - footersTotalHeight += firstFooterSize.height + footerInset + environment: {}, + containerSize: CGSize(width: itemsContainerWidth - 16.0 * 2.0, height: 1000.0) + ) + footersTotalHeight += firstFooterSize.height + footerInset + } if !editing { let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600)) @@ -1777,15 +2013,33 @@ final class ShareWithPeersScreenComponent: Component { var sections: [ItemLayout.Section] = [] if let stateValue = self.effectiveStateValue { - if case .stories = component.stateContext.subject { + if case let .peers(peers, _) = component.stateContext.subject { sections.append(ItemLayout.Section( id: 0, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), - itemHeight: categoryItemSize.height, - itemCount: component.categoryItems.count + insets: UIEdgeInsets(top: 12.0, left: 0.0, bottom: 24.0, right: 0.0), + itemHeight: peerItemSize.height, + itemCount: peers.count )) + } else if case let .stories(editing) = component.stateContext.subject { + let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1 + if !editing && sendAsPeersCount > 1 { + sections.append(ItemLayout.Section( + id: 0, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 24.0, right: 0.0), + itemHeight: peerItemSize.height, + itemCount: 1 + )) + } + if hasCategories { + sections.append(ItemLayout.Section( + id: 2, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: categoryItemSize.height, + itemCount: component.categoryItems.count + )) + } sections.append(ItemLayout.Section( - id: 2, + id: 3, insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 24.0, right: 0.0), itemHeight: optionItemSize.height, itemCount: component.optionItems.count @@ -1827,6 +2081,7 @@ final class ShareWithPeersScreenComponent: Component { base = .nobody } component.completion( + self.sendAsPeerId, EngineStoryPrivacy( base: base, additionallyIncludePeers: self.selectedPeers @@ -1854,6 +2109,8 @@ final class ShareWithPeersScreenComponent: Component { var actionButtonTitle = environment.strings.Story_Privacy_SaveList let title: String switch component.stateContext.subject { + case .peers: + title = "Post Story As" case let .stories(editing): if editing { title = environment.strings.Story_Privacy_EditStory @@ -1919,10 +2176,17 @@ final class ShareWithPeersScreenComponent: Component { if case let .stories(editing) = component.stateContext.subject { if editing { inset = 351.0 + inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight } else { - inset = 464.0 + if hasCategories { + inset = 1000.0 + } else { + inset = 314.0 + inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight + } } - inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight + } else if case .peers = component.stateContext.subject { + inset = 480.0 } else { inset = 600.0 } @@ -1934,241 +2198,255 @@ final class ShareWithPeersScreenComponent: Component { transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: navigationHeight), size: CGSize(width: containerWidth, height: UIScreenPixel))) - let badge: Int - if case .stories = component.stateContext.subject { - badge = 0 + var bottomPanelHeight: CGFloat = 0.0 + var bottomPanelInset: CGFloat = 0.0 + if case .peers = component.stateContext.subject { + } else { - badge = self.selectedPeers.count - } - - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: actionButtonTitle, - component: AnyComponent(ButtonTextContentComponent( - text: actionButtonTitle, - badge: badge, - textColor: environment.theme.list.itemCheckColors.foregroundColor, - badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, - badgeForeground: environment.theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self, let component = self.component, let environment = self.environment, let controller = self.environment?.controller() as? ShareWithPeersScreen else { - return - } - - let base: EngineStoryPrivacy.Base - if self.selectedCategories.contains(.everyone) { - base = .everyone - } else if self.selectedCategories.contains(.closeFriends) { - base = .closeFriends - } else if self.selectedCategories.contains(.contacts) { - base = .contacts - } else if self.selectedCategories.contains(.selectedContacts) { - base = .nobody - } else { - base = .nobody - } - - let proceed = { - var savePeers = true - if component.stateContext.editing { - savePeers = false - } else if base == .closeFriends { - savePeers = false + let badge: Int + if case .stories = component.stateContext.subject { + badge = 0 + } else { + badge = self.selectedPeers.count + } + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: actionButtonTitle, + component: AnyComponent(ButtonTextContentComponent( + text: actionButtonTitle, + badge: badge, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment, let controller = self.environment?.controller() as? ShareWithPeersScreen else { + return + } + + let base: EngineStoryPrivacy.Base + if self.selectedCategories.contains(.everyone) { + base = .everyone + } else if self.selectedCategories.contains(.closeFriends) { + base = .closeFriends + } else if self.selectedCategories.contains(.contacts) { + base = .contacts + } else if self.selectedCategories.contains(.selectedContacts) { + base = .nobody } else { - if case .stories = component.stateContext.subject { - savePeers = false - } else if case .chats(true) = component.stateContext.subject { - savePeers = false - } + base = .nobody } - var selectedPeers = self.selectedPeers - if case .stories = component.stateContext.subject { - if case .closeFriends = base { - selectedPeers = [] + let proceed = { + var savePeers = true + if component.stateContext.editing { + savePeers = false + } else if base == .closeFriends { + savePeers = false } else { - selectedPeers = component.stateContext.stateValue?.savedSelectedPeers[base] ?? [] - } - } - - let complete = { - let peers = component.context.engine.data.get(EngineDataMap(selectedPeers.map { id in - return TelegramEngine.EngineData.Item.Peer.Peer(id: id) - })) - - let _ = (peers - |> deliverOnMainQueue).start(next: { [weak controller, weak component] peers in - guard let controller, let component else { - return + if case .stories = component.stateContext.subject { + savePeers = false + } else if case .chats(true) = component.stateContext.subject { + savePeers = false } - component.completion( - EngineStoryPrivacy( - base: base, - additionallyIncludePeers: selectedPeers - ), - self.selectedOptions.contains(.screenshot), - self.selectedOptions.contains(.pin), - peers.values.compactMap { $0 }, - true - ) - - controller.dismissAllTooltips() - controller.dismiss() - }) - - } - if savePeers { - let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers) - |> deliverOnMainQueue).start(completed: { - complete() - }) - } else { - complete() - } - } - - let presentAlert: ([String]) -> Void = { usernames in - let usernamesString = String(usernames.map { "@\($0)" }.joined(separator: ", ")) - let alertController = textAlertController( - context: component.context, - forceTheme: defaultDarkColorPresentationTheme, - title: environment.strings.Story_Privacy_MentionRestrictedTitle, - text: environment.strings.Story_Privacy_MentionRestrictedText(usernamesString).string, - actions: [ - TextAlertAction(type: .defaultAction, title: environment.strings.Story_Privacy_MentionRestrictedProceed, action: { - proceed() - }), - TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}) - ], - actionLayout: .vertical - ) - controller.present(alertController, in: .window(.root)) - } - - func matchingUsername(user: TelegramUser, usernames: Set) -> String? { - for username in user.usernames { - if usernames.contains(username.username) { - return username.username } - } - if let username = user.username { - if usernames.contains(username) { - return username + + var selectedPeers = self.selectedPeers + if case .stories = component.stateContext.subject { + if case .closeFriends = base { + selectedPeers = [] + } else { + selectedPeers = component.stateContext.stateValue?.savedSelectedPeers[base] ?? [] + } } - } - return nil - } - - let context = component.context - let selectedPeerIds = self.selectedPeers - - if case .stories = component.stateContext.subject { - if component.mentions.isEmpty { - proceed() - } else if case .nobody = base { - if selectedPeerIds.isEmpty { - presentAlert(component.mentions) + + let complete = { + let peers = component.context.engine.data.get(EngineDataMap(selectedPeers.map { id in + return TelegramEngine.EngineData.Item.Peer.Peer(id: id) + })) + + let _ = (peers + |> deliverOnMainQueue).start(next: { [weak controller, weak component] peers in + guard let controller, let component else { + return + } + component.completion( + self.sendAsPeerId, + EngineStoryPrivacy( + base: base, + additionallyIncludePeers: selectedPeers + ), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin), + peers.values.compactMap { $0 }, + true + ) + + controller.dismissAllTooltips() + controller.dismiss() + }) + + } + if savePeers { + let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers) + |> deliverOnMainQueue).start(completed: { + complete() + }) } else { - let _ = (context.account.postbox.transaction { transaction in + complete() + } + } + + let presentAlert: ([String]) -> Void = { usernames in + let usernamesString = String(usernames.map { "@\($0)" }.joined(separator: ", ")) + let alertController = textAlertController( + context: component.context, + forceTheme: defaultDarkColorPresentationTheme, + title: environment.strings.Story_Privacy_MentionRestrictedTitle, + text: environment.strings.Story_Privacy_MentionRestrictedText(usernamesString).string, + actions: [ + TextAlertAction(type: .defaultAction, title: environment.strings.Story_Privacy_MentionRestrictedProceed, action: { + proceed() + }), + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + controller.present(alertController, in: .window(.root)) + } + + func matchingUsername(user: TelegramUser, usernames: Set) -> String? { + for username in user.usernames { + if usernames.contains(username.username) { + return username.username + } + } + if let username = user.username { + if usernames.contains(username) { + return username + } + } + return nil + } + + let context = component.context + let selectedPeerIds = self.selectedPeers + + if case .stories = component.stateContext.subject { + if component.mentions.isEmpty { + proceed() + } else if case .nobody = base { + if selectedPeerIds.isEmpty { + presentAlert(component.mentions) + } else { + let _ = (context.account.postbox.transaction { transaction in + var filteredMentions = Set(component.mentions) + for peerId in selectedPeerIds { + if let peer = transaction.getPeer(peerId) { + if let user = peer as? TelegramUser { + if let username = matchingUsername(user: user, usernames: filteredMentions) { + filteredMentions.remove(username) + } + } else { + if let username = peer.addressName { + filteredMentions.remove(username) + } + } + } + } + return Array(filteredMentions) + } + |> deliverOnMainQueue).start(next: { mentions in + if mentions.isEmpty { + proceed() + } else { + presentAlert(mentions) + } + }) + } + } else if case .contacts = base { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) + |> map { contacts -> [String] in var filteredMentions = Set(component.mentions) - for peerId in selectedPeerIds { - if let user = transaction.getPeer(peerId) as? TelegramUser, let username = matchingUsername(user: user, usernames: filteredMentions) { + let peers = contacts.peers + for peer in peers { + if selectedPeerIds.contains(peer.id) { + continue + } + if case let .user(user) = peer, let username = matchingUsername(user: user, usernames: filteredMentions) { filteredMentions.remove(username) } } return Array(filteredMentions) } - |> deliverOnMainQueue).start(next: { mentions in + |> deliverOnMainQueue).start(next: { mentions in if mentions.isEmpty { proceed() } else { presentAlert(mentions) } }) - } - } else if case .contacts = base { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) - |> map { contacts -> [String] in - var filteredMentions = Set(component.mentions) - let peers = contacts.peers - for peer in peers { - if selectedPeerIds.contains(peer.id) { - continue + } else if case .closeFriends = base { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) + |> map { contacts -> [String] in + var filteredMentions = Set(component.mentions) + let peers = contacts.peers + for peer in peers { + if case let .user(user) = peer, user.flags.contains(.isCloseFriend), let username = matchingUsername(user: user, usernames: filteredMentions) { + filteredMentions.remove(username) + } } - if case let .user(user) = peer, let username = matchingUsername(user: user, usernames: filteredMentions) { - filteredMentions.remove(username) + return Array(filteredMentions) + } + |> deliverOnMainQueue).start(next: { mentions in + if mentions.isEmpty { + proceed() + } else { + presentAlert(mentions) } - } - return Array(filteredMentions) + }) + } else { + proceed() } - |> deliverOnMainQueue).start(next: { mentions in - if mentions.isEmpty { - proceed() - } else { - presentAlert(mentions) - } - }) - } else if case .closeFriends = base { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) - |> map { contacts -> [String] in - var filteredMentions = Set(component.mentions) - let peers = contacts.peers - for peer in peers { - if case let .user(user) = peer, user.flags.contains(.isCloseFriend), let username = matchingUsername(user: user, usernames: filteredMentions) { - filteredMentions.remove(username) - } - } - return Array(filteredMentions) - } - |> deliverOnMainQueue).start(next: { mentions in - if mentions.isEmpty { - proceed() - } else { - presentAlert(mentions) - } - }) } else { proceed() } - } else { - proceed() } - } - )), - environment: {}, - containerSize: CGSize(width: containerWidth - navigationSideInset * 2.0, height: 50.0) - ) - - var bottomPanelHeight: CGFloat = 0.0 - if environment.inputHeight != 0.0 { - bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height - } else { - bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height - } - let actionButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.containerView.addSubview(actionButtonView) + )), + environment: {}, + containerSize: CGSize(width: containerWidth - navigationSideInset * 2.0, height: 50.0) + ) + + if environment.inputHeight != 0.0 { + bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height + } else { + bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + let actionButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.containerView.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + bottomPanelInset = 8.0 + transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: containerWidth, height: bottomPanelHeight + bottomPanelInset))) + self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) } - let bottomPanelInset: CGFloat = 8.0 - transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: containerWidth, height: bottomPanelHeight + bottomPanelInset))) - self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) - let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height) let itemLayout = ItemLayout(style: itemLayoutStyle, containerSize: itemContainerSize, containerInset: containerInset, bottomInset: footersTotalHeight, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) let previousItemLayout = self.itemLayout @@ -2236,6 +2514,7 @@ final class ShareWithPeersScreenComponent: Component { public class ShareWithPeersScreen: ViewControllerComponentContainer { public final class State { + let sendAsPeers: [EnginePeer] let peers: [EnginePeer] let peersMap: [EnginePeer.Id: EnginePeer] let savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] @@ -2245,6 +2524,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { let grayListPeers: [EnginePeer] fileprivate init( + sendAsPeers: [EnginePeer], peers: [EnginePeer], peersMap: [EnginePeer.Id: EnginePeer], savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]], @@ -2253,6 +2533,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { closeFriendsPeers: [EnginePeer], grayListPeers: [EnginePeer] ) { + self.sendAsPeers = sendAsPeers self.peers = peers self.peersMap = peersMap self.savedSelectedPeers = savedSelectedPeers @@ -2265,6 +2546,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public final class StateContext { public enum Subject: Equatable { + case peers(peers: [EnginePeer], peerId: EnginePeer.Id?) case stories(editing: Bool) case chats(blocked: Bool) case contacts(base: EngineStoryPrivacy.Base) @@ -2295,6 +2577,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], initialPeerIds: Set = Set(), closeFriends: Signal<[EnginePeer], NoError> = .single([]), + adminedChannels: Signal<[EnginePeer], NoError> = .single([]), blockedPeersContext: BlockedPeersContext? = nil ) { self.subject = subject @@ -2313,6 +2596,21 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } switch subject { + case let .peers(peers, _): + let state = State( + sendAsPeers: peers, + peers: [], + peersMap: [:], + savedSelectedPeers: [:], + presences: [:], + participants: [:], + closeFriendsPeers: [], + grayListPeers: [] + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) case .stories: let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone) let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts) @@ -2387,14 +2685,22 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.stateDisposable = combineLatest( queue: Queue.mainQueue(), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + adminedChannels, savedPeers, closeFriends, grayListPeers ) - .start(next: { [weak self] savedPeers, closeFriends, grayListPeers in + .start(next: { [weak self] accountPeer, adminedChannels, savedPeers, closeFriends, grayListPeers in guard let self else { return } + + var sendAsPeers: [EnginePeer] = [] + if let accountPeer { + sendAsPeers.append(accountPeer) + } + sendAsPeers.append(contentsOf: adminedChannels) let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:] @@ -2402,6 +2708,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { savedSelectedPeers[.contacts] = contactsPeers savedSelectedPeers[.nobody] = selectedPeers let state = State( + sendAsPeers: sendAsPeers, peers: [], peersMap: peersMap, savedSelectedPeers: savedSelectedPeers, @@ -2520,6 +2827,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { peers.insert(contentsOf: selectedPeers, at: 0) let state = State( + sendAsPeers: [], peers: peers, peersMap: [:], savedSelectedPeers: [:], @@ -2585,6 +2893,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { peers.insert(contentsOf: selectedPeers, at: 0) let state = State( + sendAsPeers: [], peers: peers, peersMap: [:], savedSelectedPeers: [:], @@ -2643,6 +2952,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } let state = State( + sendAsPeers: [], peers: peers.compactMap { $0.peer }.filter { peer in if case let .user(user) = peer { if user.id == context.account.peerId { @@ -2703,14 +3013,16 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public init( context: AccountContext, initialPrivacy: EngineStoryPrivacy, + initialSendAsPeerId: EnginePeer.Id? = nil, allowScreenshots: Bool = true, pin: Bool = false, timeout: Int = 0, mentions: [String] = [], stateContext: StateContext, - completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, + completion: @escaping (EnginePeer.Id?, EngineStoryPrivacy, Bool, Bool, [EnginePeer], Bool) -> Void, editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, - editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void + editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void, + peerCompletion: @escaping (EnginePeer.Id) -> Void = { _ in } ) { self.context = context @@ -2857,6 +3169,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { context: context, stateContext: stateContext, initialPrivacy: initialPrivacy, + initialSendAsPeerId: initialSendAsPeerId, screenshot: allowScreenshots, pin: pin, timeout: timeout, @@ -2865,7 +3178,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { optionItems: optionItems, completion: completion, editCategory: editCategory, - editBlockedPeers: editBlockedPeers + editBlockedPeers: editBlockedPeers, + peerCompletion: peerCompletion ), navigationBarAppearance: .none, theme: .dark) self.statusBar.statusBarStyle = .Ignore diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 1289eaf99c..7180382396 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -21,6 +21,8 @@ import TextFormat private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) +private let checkImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)?.withRenderingMode(.alwaysTemplate) +private let disclosureImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) public final class PeerListItemComponent: Component { public final class TransitionHint { @@ -46,6 +48,12 @@ public final class PeerListItemComponent: Component { case checks } + public enum RightAccessory: Equatable { + case none + case disclosure + case check + } + public final class Reaction: Equatable { public let reaction: MessageReaction.Reaction public let file: TelegramMediaFile? @@ -90,6 +98,7 @@ public final class PeerListItemComponent: Component { let subtitle: String? let subtitleAccessory: SubtitleAccessory let presence: EnginePeer.Presence? + let rightAccessory: RightAccessory let reaction: Reaction? let selectionState: SelectionState let hasNext: Bool @@ -109,6 +118,7 @@ public final class PeerListItemComponent: Component { subtitle: String?, subtitleAccessory: SubtitleAccessory, presence: EnginePeer.Presence?, + rightAccessory: RightAccessory = .none, reaction: Reaction? = nil, selectionState: SelectionState, hasNext: Bool, @@ -127,6 +137,7 @@ public final class PeerListItemComponent: Component { self.subtitle = subtitle self.subtitleAccessory = subtitleAccessory self.presence = presence + self.rightAccessory = rightAccessory self.reaction = reaction self.selectionState = selectionState self.hasNext = hasNext @@ -169,6 +180,9 @@ public final class PeerListItemComponent: Component { if lhs.presence != rhs.presence { return false } + if lhs.rightAccessory != rhs.rightAccessory { + return false + } if lhs.reaction != rhs.reaction { return false } @@ -482,7 +496,7 @@ public final class PeerListItemComponent: Component { let avatarSize: CGFloat = component.style == .compact ? 30.0 : 40.0 - let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floorToScreenPixels((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) if self.avatarNode.bounds.isEmpty { self.avatarNode.frame = avatarFrame } else { @@ -570,11 +584,10 @@ public final class PeerListItemComponent: Component { ) let titleSpacing: CGFloat = 2.0 - var titleVerticalOffset: CGFloat = 0.0 + let titleVerticalOffset: CGFloat = 0.0 let centralContentHeight: CGFloat if labelSize.height > 0.0, case .generic = component.style { centralContentHeight = titleSize.height + labelSize.height + titleSpacing - titleVerticalOffset = -1.0 } else { centralContentHeight = titleSize.height } @@ -686,6 +699,40 @@ public final class PeerListItemComponent: Component { let imageSize = CGSize(width: 22.0, height: 22.0) self.iconFrame = CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 14.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize) + if case .none = component.rightAccessory { + if case .none = component.subtitleAccessory { + if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + } + } + } else { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + var image: UIImage? + var color: UIColor = component.theme.list.itemSecondaryTextColor + switch component.rightAccessory { + case .check: + image = checkImage + color = component.theme.list.itemAccentColor + case .disclosure: + image = disclosureImage + case .none: + break + } + iconView = UIImageView(image: image) + iconView.tintColor = color + self.iconView = iconView + self.containerButton.addSubview(iconView) + } + + if let image = iconView.image { + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: availableSize.width - image.size.width, y: floor((height - verticalInset * 2.0 - image.size.width) / 2.0)), size: image.size)) + } + } + var reactionIconTransition = transition if previousComponent?.reaction != component.reaction { if let reaction = component.reaction, case .builtin("❤") = reaction.reaction { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index e3e401561f..59a997e30a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -74,6 +74,7 @@ final class StoryItemOverlaysView: UIView { func update( context: AccountContext, reaction: MessageReaction.Reaction, + flags: MediaArea.ReactionFlags, availableReactions: StoryAvailableReactions?, synchronous: Bool, size: CGSize @@ -84,6 +85,12 @@ final class StoryItemOverlaysView: UIView { self.coverView.frame = CGRect(origin: CGPoint(x: size.width * insets.left, y: size.height * insets.top), size: CGSize(width: size.width - size.width * insets.left - size.width * insets.right, height: size.height - size.height * insets.top - size.height * insets.bottom)) self.shadowView.frame = self.coverView.frame + if flags.contains(.isFlipped) { + self.coverView.transform = CGAffineTransformMakeScale(-1.0, 1.0) + self.shadowView.transform = self.coverView.transform + } + self.coverView.tintColor = flags.contains(.isDark) ? UIColor(rgb: 0x000000, alpha: 0.5) : UIColor.white + let minSide = floor(min(200.0, min(size.width, size.height)) * 0.65) let itemSize = CGSize(width: minSide, height: minSide) @@ -172,7 +179,7 @@ final class StoryItemOverlaysView: UIView { var nextId = 0 for mediaArea in story.mediaAreas { switch mediaArea { - case let .reaction(coordinates, reaction): + case let .reaction(coordinates, reaction, flags): let referenceSize = size let areaSize = CGSize(width: coordinates.width / 100.0 * referenceSize.width, height: coordinates.height / 100.0 * referenceSize.height) let targetFrame = CGRect(x: coordinates.x / 100.0 * referenceSize.width - areaSize.width * 0.5, y: coordinates.y / 100.0 * referenceSize.height - areaSize.height * 0.5, width: areaSize.width, height: areaSize.height) @@ -192,13 +199,14 @@ final class StoryItemOverlaysView: UIView { self.itemViews[itemId] = itemView self.addSubview(itemView) } - + transition.setPosition(view: itemView, position: targetFrame.center) transition.setBounds(view: itemView, bounds: CGRect(origin: CGPoint(), size: targetFrame.size)) transition.setTransform(view: itemView, transform: CATransform3DMakeRotation(coordinates.rotation * (CGFloat.pi / 180.0), 0.0, 0.0, 1.0)) itemView.update( context: context, reaction: reaction, + flags: flags, availableReactions: availableReactions, synchronous: attemptSynchronous, size: targetFrame.size diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 0326b820ac..fc01e1b627 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4577,7 +4577,7 @@ public final class StoryItemSetContainerComponent: Component { context: context, initialPrivacy: privacy, stateContext: stateContext, - completion: { [weak self] privacy, _, _, _, completed in + completion: { [weak self] _, privacy, _, _, _, completed in guard let self, let component = self.component, completed else { return } @@ -4658,7 +4658,7 @@ public final class StoryItemSetContainerComponent: Component { context: context, initialPrivacy: privacy, stateContext: stateContext, - completion: { [weak self] result, _, _, peers, completed in + completion: { [weak self] _, result, _, _, peers, completed in guard completed else { return } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 802e9baf64..fb692de00a 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -368,7 +368,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } else { return nil } - }, completion: { [weak self] randomId, mediaResult, mediaAreas, caption, privacy, stickers, commit in + }, completion: { [weak self] randomId, mediaResult, mediaAreas, caption, options, stickers, commit in guard let self, let mediaResult else { dismissCameraImpl?() commit({}) @@ -419,7 +419,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let imageData = compressImageToJPEG(image, quality: 0.7) { let entities = generateChatInputTextEntities(caption) Logger.shared.log("MediaEditor", "Calling uploadStory for image, randomId \(randomId)") - let _ = (context.engine.messages.uploadStory(target: target, media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) + let _ = (context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) |> deliverOnMainQueue).start(next: { stableId in moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId)) }) @@ -453,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } Logger.shared.log("MediaEditor", "Calling uploadStory for video, randomId \(randomId)") let entities = generateChatInputTextEntities(caption) - let _ = (context.engine.messages.uploadStory(target: target, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: privacy.pin, privacy: privacy.privacy, isForwardingDisabled: privacy.isForwardingDisabled, period: privacy.timeout, randomId: randomId) + let _ = (context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: stickers), mediaAreas: mediaAreas, text: caption.string, entities: entities, pin: options.pin, privacy: options.privacy, isForwardingDisabled: options.isForwardingDisabled, period: options.timeout, randomId: randomId) |> deliverOnMainQueue).start(next: { stableId in moveStorySource(engine: context.engine, peerId: context.account.peerId, from: randomId, to: Int64(stableId)) }) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index d0141cf693..2e3f0d07d6 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1135,7 +1135,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "phone_requested", data: paramsString) } - controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { sendEvent(false) }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { @@ -1150,7 +1150,13 @@ public final class WebAppController: ViewController, AttachmentContainable { sendEvent(true) } }) - })]), in: .window(.root)) + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + sendEvent(false) + } + } + controller.present(alertController, in: .window(.root)) } fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) {