diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 39c216d423..ed0c94423e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14916,3 +14916,5 @@ Sorry for the inconvenience."; "Conversation.StopVoiceMessageDescription" = "Are you sure you want to pause recording?"; "Conversation.StopVoiceMessageDiscardAction" = "Discard"; "Conversation.StopVoiceMessagePauseAction" = "Pause"; + +"Gift.Options.GiftLocked.Title" = "Gift Locked"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index fbba79185a..ccd93181b5 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1179,7 +1179,7 @@ public enum ChatHistoryListSource { } case `default` - case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, updateAll: Bool, canReorder: Bool, loadMore: (() -> Void)?) + case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId?, quote: Quote?, isSavedMusic: Bool, canReorder: Bool, loadMore: (() -> Void)?) case customView(historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError>) } diff --git a/submodules/AccountContext/Sources/MediaManager.swift b/submodules/AccountContext/Sources/MediaManager.swift index dba8c26977..83c094b4a4 100644 --- a/submodules/AccountContext/Sources/MediaManager.swift +++ b/submodules/AccountContext/Sources/MediaManager.swift @@ -12,6 +12,7 @@ public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId { case peer(PeerId) case recentActions(PeerId) case feed(Int32) + case savedMusic(PeerId) case custom public func isEqual(to: SharedMediaPlaylistId) -> Bool { @@ -26,6 +27,7 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation case messages(chatLocation: ChatLocation, tagMask: MessageTags, at: MessageId) case singleMessage(MessageId) case recentActions(Message) + case savedMusic(context: ProfileSavedMusicContext, at: Int32, canReorder: Bool) case custom(messages: Signal<([Message], Int32, Bool), NoError>, canReorder: Bool, at: MessageId, loadMore: (() -> Void)?) public var playlistId: PeerMessagesMediaPlaylistId { @@ -43,11 +45,49 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation return .peer(id.peerId) case let .recentActions(message): return .recentActions(message.id.peerId) + case let .savedMusic(context, _, _): + return .savedMusic(context.peerId) case .custom: return .custom } } + public func effectiveLocation(context: AccountContext) -> PeerMessagesPlaylistLocation { + switch self { + case let .savedMusic(savedMusicContext, at, canReorder): + let peerId = savedMusicContext.peerId + return .custom( + messages: combineLatest( + savedMusicContext.state, + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: savedMusicContext.peerId)) + ) + |> map { state, peer in + var messages: [Message] = [] + var peers = SimpleDictionary() + if let peer { + peers[peerId] = peer._asPeer() + } + for file in state.files { + let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max)) + messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])) + + } + return (messages, Int32(messages.count), true) + }, + canReorder: canReorder, + at: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: at), + loadMore: { [weak savedMusicContext] in + guard let savedMusicContext else { + return + } + savedMusicContext.loadMore() + } + ) + default: + return self + } + } + public var messageId: MessageId? { switch self { case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, _, messageId, _): @@ -85,6 +125,12 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation } else { return false } + case let .savedMusic(lhsContext, lhsAt, _): + if case let .savedMusic(rhsContext, rhsAt, _) = rhs { + return lhsContext.peerId == rhsContext.peerId && lhsAt == rhsAt + } else { + return false + } case let .custom(_, _, lhsAt, _): if case let .custom(_, _, rhsAt, _) = rhs, lhsAt == rhsAt { return true @@ -120,8 +166,10 @@ public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManager return nil } -public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { - if isGlobalSearch && !isDownloadList { +public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { + if isSavedMusic { + return (PeerMessagesMediaPlaylistId.savedMusic(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) + } else if isGlobalSearch && !isDownloadList { return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) } else if isRecentActions && !isDownloadList { return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) diff --git a/submodules/ComponentFlow/Source/Components/Text.swift b/submodules/ComponentFlow/Source/Components/Text.swift index b364de1224..65d9e81587 100644 --- a/submodules/ComponentFlow/Source/Components/Text.swift +++ b/submodules/ComponentFlow/Source/Components/Text.swift @@ -30,12 +30,16 @@ public final class Text: Component { public final class View: UIView { private var measureState: MeasureState? - public func update(component: Text, availableSize: CGSize) -> CGSize { + public func update(component: Text, availableSize: CGSize, transition: ComponentTransition) -> CGSize { let attributedText = NSAttributedString(string: component.text, attributes: [ NSAttributedString.Key.font: component.font, NSAttributedString.Key.foregroundColor: component.color ]) + if let tintColor = component.tintColor { + transition.setTintColor(layer: self.layer, color: tintColor) + } + if let measureState = self.measureState { if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize { return measureState.size @@ -71,11 +75,13 @@ public final class Text: Component { public let text: String public let font: UIFont public let color: UIColor + public let tintColor: UIColor? - public init(text: String, font: UIFont, color: UIColor) { + public init(text: String, font: UIFont, color: UIColor, tintColor: UIColor? = nil) { self.text = text self.font = font self.color = color + self.tintColor = tintColor } public static func ==(lhs: Text, rhs: Text) -> Bool { @@ -88,6 +94,9 @@ public final class Text: Component { if !lhs.color.isEqual(rhs.color) { return false } + if lhs.tintColor != rhs.tintColor { + return false + } return true } @@ -96,6 +105,6 @@ public final class Text: Component { } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize) + return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift index 30453c2122..3908f9ae06 100644 --- a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -19,6 +19,7 @@ public final class BalancedTextComponent: Component { public let lineSpacing: CGFloat public let cutout: TextNodeCutout? public let insets: UIEdgeInsets + public let tintColor: UIColor? public let textShadowColor: UIColor? public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? @@ -38,6 +39,7 @@ public final class BalancedTextComponent: Component { lineSpacing: CGFloat = 0.0, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), + tintColor: UIColor? = nil, textShadowColor: UIColor? = nil, textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, @@ -56,6 +58,7 @@ public final class BalancedTextComponent: Component { self.lineSpacing = lineSpacing self.cutout = cutout self.insets = insets + self.tintColor = tintColor self.textShadowColor = textShadowColor self.textShadowBlur = textShadowBlur self.textStroke = textStroke @@ -94,7 +97,9 @@ public final class BalancedTextComponent: Component { if lhs.insets != rhs.insets { return false } - + if lhs.tintColor != rhs.tintColor { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -202,6 +207,10 @@ public final class BalancedTextComponent: Component { bestSize = (availableSize.width, bestInfo) } + if let tintColor = component.tintColor { + transition.setTintColor(layer: self.textView.layer, color: tintColor) + } + self.textView.frame = CGRect(origin: CGPoint(), size: bestSize.info.size) return bestSize.info.size } diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index cfcbaf9b85..715d8cac67 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -18,6 +18,7 @@ public final class MultilineTextComponent: Component { public let lineSpacing: CGFloat public let cutout: TextNodeCutout? public let insets: UIEdgeInsets + public let tintColor: UIColor? public let textShadowColor: UIColor? public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? @@ -36,6 +37,7 @@ public final class MultilineTextComponent: Component { lineSpacing: CGFloat = 0.0, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), + tintColor: UIColor? = nil, textShadowColor: UIColor? = nil, textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, @@ -53,6 +55,7 @@ public final class MultilineTextComponent: Component { self.lineSpacing = lineSpacing self.cutout = cutout self.insets = insets + self.tintColor = tintColor self.textShadowColor = textShadowColor self.textShadowBlur = textShadowBlur self.textStroke = textStroke @@ -88,7 +91,9 @@ public final class MultilineTextComponent: Component { if lhs.insets != rhs.insets { return false } - + if lhs.tintColor != rhs.tintColor { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -169,6 +174,10 @@ public final class MultilineTextComponent: Component { let size = self.updateLayout(availableSize) + if let tintColor = component.tintColor { + transition.setTintColor(layer: self.layer, color: tintColor) + } + return size } } diff --git a/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift b/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift index 5c7fb53fd9..a9e00836eb 100644 --- a/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift +++ b/submodules/FileMediaResourceStatus/Sources/FileMediaResourceStatus.swift @@ -5,12 +5,12 @@ import SwiftSignalKit import UniversalMediaPlayer import AccountContext -private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal { +private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> Signal { guard let playerType = peerMessageMediaPlayerType(message) else { return .single(nil) } - if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) { + if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList, isSavedMusic: isSavedMusic) { return context.sharedContext.mediaManager.filteredPlaylistState(accountId: context.account.id, playlistId: playlistId, itemId: itemId, type: playerType) |> mapToSignal { state -> Signal in return .single(state?.status) @@ -20,32 +20,32 @@ private func internalMessageFileMediaPlaybackStatus(context: AccountContext, fil } } -public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal { +public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> Signal { var duration = 0.0 if let value = file.duration { duration = Double(value) } let defaultStatus = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true) - return internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) + return internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList, isSavedMusic: isSavedMusic) |> map { status in return status ?? defaultStatus } } -public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal { +public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> Signal { guard let playerType = peerMessageMediaPlayerType(message) else { return .never() } - if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) { + if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList, isSavedMusic: isSavedMusic) { return context.sharedContext.mediaManager.filteredPlayerAudioLevelEvents(accountId: context.account.id, playlistId: playlistId, itemId: itemId, type: playerType) } else { return .never() } } -public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal { - let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in +public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false) -> Signal { + let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList, isSavedMusic: isSavedMusic) |> map { status -> MediaPlayerPlaybackStatus? in return status?.status } diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index fe3759663f..be9aee83dc 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -684,7 +684,7 @@ private final class PendingInAppPurchaseState: Codable { case gift(peerId: EnginePeer.Id) case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) - case stars(count: Int64) + case stars(count: Int64, peerId: EnginePeer.Id?) case starsGift(peerId: EnginePeer.Id, count: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, users: Int32) case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String) @@ -724,7 +724,8 @@ private final class PendingInAppPurchaseState: Codable { ) case .stars: self = .stars( - count: try container.decode(Int64.self, forKey: .stars) + count: try container.decode(Int64.self, forKey: .stars), + peerId: try container.decodeIfPresent(Int64.self, forKey: .peer).flatMap { EnginePeer.Id($0) } ) case .starsGift: self = .starsGift( @@ -784,9 +785,10 @@ private final class PendingInAppPurchaseState: Codable { try container.encodeIfPresent(prizeDescription, forKey: .prizeDescription) try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) - case let .stars(count): + case let .stars(count, peerId): try container.encode(PurposeType.stars.rawValue, forKey: .type) try container.encode(count, forKey: .stars) + try container.encodeIfPresent(peerId?.toInt64(), forKey: .peer) case let .starsGift(peerId, count): try container.encode(PurposeType.starsGift.rawValue, forKey: .type) try container.encode(peerId.toInt64(), forKey: .peer) @@ -825,8 +827,8 @@ private final class PendingInAppPurchaseState: Codable { self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _): self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) - case let .stars(count, _, _): - self = .stars(count: count) + case let .stars(count, _, _, peerId): + self = .stars(count: count, peerId: peerId) case let .starsGift(peerId, count, _, _): self = .starsGift(peerId: peerId, count: count) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _, users): @@ -851,8 +853,8 @@ private final class PendingInAppPurchaseState: Codable { return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) - case let .stars(count): - return .stars(count: count, currency: currency, amount: amount) + case let .stars(count, peerId): + return .stars(count: count, currency: currency, amount: amount, peerId: peerId) case let .starsGift(peerId, count): return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users): diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index b1e68a51c4..0de2906f25 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -907,7 +907,7 @@ public final class ListMessageFileItemNode: ListMessageNode { if statusUpdated && item.displayFileInfo { if let file = selectedMedia as? TelegramMediaFile { - updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || message.id.namespace == Namespaces.Message.Local, isDownloadList: item.isDownloadList) + updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList, isSavedMusic: item.isSavedMusic) |> mapToSignal { value -> Signal in if case .Fetching = value.fetchStatus, !item.isDownloadList { return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) @@ -934,7 +934,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } } if isVoice { - updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList, isSavedMusic: false) } } else if let image = selectedMedia as? TelegramMediaImage { updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList) diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index 57e6d972ba..5968f972ed 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -55,6 +55,7 @@ public final class ListMessageItem: ListViewItem { let hintIsLink: Bool let isGlobalSearchResult: Bool let isDownloadList: Bool + let isSavedMusic: Bool let displayFileInfo: Bool let displayBackground: Bool let canReorder: Bool @@ -64,7 +65,7 @@ public final class ListMessageItem: ListViewItem { public let selectable: Bool = true - public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) { + public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) { self.presentationData = presentationData self.context = context self.chatLocation = chatLocation @@ -82,6 +83,7 @@ public final class ListMessageItem: ListViewItem { self.hintIsLink = hintIsLink self.isGlobalSearchResult = isGlobalSearchResult self.isDownloadList = isDownloadList + self.isSavedMusic = isSavedMusic self.displayFileInfo = displayFileInfo self.displayBackground = displayBackground self.canReorder = canReorder diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index b543d3f8fa..3526956117 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -853,7 +853,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation { if type == .music { - if case .custom = playlistLocation { + switch playlistLocation { + case .custom, .savedMusic: let controllerContext: AccountContext if account.id == strongSelf.context.account.id { controllerContext = strongSelf.context @@ -863,7 +864,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, chatLocation: .peer(id: id.messageId.peerId), type: type, initialMessageId: id.messageId, initialOrder: order, playlistLocation: playlistLocation, parentNavigationController: strongSelf.navigationController as? NavigationController, updateMusicSaved: nil, reorderSavedMusic: nil) strongSelf.displayNode.view.window?.endEditing(true) strongSelf.present(controller, in: .window(.root)) - } else if case let .messages(chatLocation, _, _) = playlistLocation { + case let .messages(chatLocation, _, _): let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic(value: nil), tag: .tag(MessageTags.music)) var cancelImpl: (() -> Void)? @@ -915,6 +916,8 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { cancelImpl = { self?.playlistPreloadDisposable?.dispose() } + default: + break } } else { strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) diff --git a/submodules/TelegramCore/FlatSerialization/Models/PartialMediaReference.fbs b/submodules/TelegramCore/FlatSerialization/Models/PartialMediaReference.fbs index 65fb77a3ba..852e58faf1 100644 --- a/submodules/TelegramCore/FlatSerialization/Models/PartialMediaReference.fbs +++ b/submodules/TelegramCore/FlatSerialization/Models/PartialMediaReference.fbs @@ -41,13 +41,18 @@ table PartialMediaReference_SavedSticker { table PartialMediaReference_RecentSticker { } +table PartialMediaReference_SavedMusic { + peer:PeerReference (id: 0); +} + union PartialMediaReference_Value { PartialMediaReference_Message, PartialMediaReference_WebPage, PartialMediaReference_StickerPack, PartialMediaReference_SavedGif, PartialMediaReference_SavedSticker, - PartialMediaReference_RecentSticker + PartialMediaReference_RecentSticker, + PartialMediaReference_SavedMusic } table PartialMediaReference { diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 1123cec7b9..2e9687ce88 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -566,7 +566,12 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if transformedMedia { infoFlags.insert(.transformedMedia) } - attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: infoFlags, acknowledged: false, correlationId: message.correlationId, bubbleUpEmojiOrStickersets: message.bubbleUpEmojiOrStickersets)) + + var partialReference: PartialMediaReference? + if case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = message { + partialReference = mediaReference?.partial + } + attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: infoFlags, acknowledged: false, correlationId: message.correlationId, bubbleUpEmojiOrStickersets: message.bubbleUpEmojiOrStickersets, partialReference: partialReference)) globallyUniqueIds.append(randomId) switch message { diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 09d8406173..1940b8d05e 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -70,6 +70,8 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po var contextResult: OutgoingChatContextResultMessageAttribute? var autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute? var autoclearMessageAttribute: AutoclearTimeoutMessageAttribute? + var forwardInfo: ForwardSourceInfoAttribute? + var explicitPartialReference: PartialMediaReference? for attribute in attributes { if let attribute = attribute as? OutgoingChatContextResultMessageAttribute { if peerId.namespace != Namespaces.Peer.SecretChat { @@ -79,18 +81,17 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po autoremoveMessageAttribute = attribute } else if let attribute = attribute as? AutoclearTimeoutMessageAttribute { autoclearMessageAttribute = attribute - } - } - - var forwardInfo: ForwardSourceInfoAttribute? - for attribute in attributes { - if let attribute = attribute as? ForwardSourceInfoAttribute { + } else if let attribute = attribute as? OutgoingMessageInfoAttribute { + if let partialReference = attribute.partialReference { + explicitPartialReference = partialReference + } + } else if let attribute = attribute as? ForwardSourceInfoAttribute { if peerId.namespace != Namespaces.Peer.SecretChat { forwardInfo = attribute } } } - + if let media = media.first as? TelegramMediaAction, media.action == .historyScreenshot { return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .messageScreenshot, reuploadInfo: nil, cacheReferenceKey: nil)), .none) } else if let forwardInfo = forwardInfo { @@ -121,14 +122,14 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil)) } |> castError(PendingMessageUploadError.self), .text) - } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes, mediaReference: mediaReference) { + } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes, mediaReference: mediaReference, explicitPartialReference: explicitPartialReference) { return .signal(mediaResult, .media) } else { return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text) } } -func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute], mediaReference: AnyMediaReference?) -> Signal? { +func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute], mediaReference: AnyMediaReference?, explicitPartialReference: PartialMediaReference?) -> Signal? { if let paidContent = media as? TelegramMediaPaidContent { var signals: [Signal] = [] var mediaIds: [MediaId] = [] @@ -217,6 +218,8 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post return .standalone(media: file) } } + } else if let explicitPartialReference { + finalMediaReference = .single(explicitPartialReference.mediaReference(file)) } else { finalMediaReference = .single(.savedGif(media: file)) } @@ -580,7 +583,7 @@ if "".isEmpty { let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) } else { - updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [], partialReference: nil)) } } @@ -1031,7 +1034,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili let attribute = updatedAttributes[index] as! OutgoingMessageInfoAttribute updatedAttributes[index] = attribute.withUpdatedFlags(attribute.flags.union([.transformedMedia])) } else { - updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + updatedAttributes.append(OutgoingMessageInfoAttribute(uniqueId: Int64.random(in: Int64.min ... Int64.max), flags: [.transformedMedia], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [], partialReference: nil)) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift index 864d295311..5c5f8a7cc9 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift @@ -120,7 +120,7 @@ private func generatePeerMediaMessage(network: Network, accountPeerId: EnginePee flags.insert(.Sending) var attributes: [MessageAttribute] = [] - attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: [], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: [], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [], partialReference: nil)) var media: [Media] = [] switch content { diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 15cb2eea6f..7316f7234f 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -66,7 +66,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, if let invertMediaAttribute { attributes.append(invertMediaAttribute) } - return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil) + return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil, explicitPartialReference: nil) } if let todo = media.media as? TelegramMediaTodo { var flags: Int32 = 0 diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index d94f674edb..d5be2f2137 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -388,8 +388,8 @@ public enum AnyMediaReference: Equatable { return nil case .starsTransaction: return nil - case .savedMusic: - return nil + case let .savedMusic(peer, _): + return .savedMusic(peer: peer) } } @@ -526,6 +526,7 @@ public enum PartialMediaReference: Equatable { case savedGif case savedSticker case recentSticker + case savedMusic } case message(message: MessageReference) @@ -534,6 +535,7 @@ public enum PartialMediaReference: Equatable { case savedGif case savedSticker case recentSticker + case savedMusic(peer: PeerReference) public init?(decoder: PostboxDecoder) { guard let caseIdValue = decoder.decodeOptionalInt32ForKey("_r"), let caseId = CodingCase(rawValue: caseIdValue) else { @@ -555,6 +557,9 @@ public enum PartialMediaReference: Equatable { self = .savedSticker case .recentSticker: self = .recentSticker + case .savedMusic: + let peer = decoder.decodeObjectForKey("pg", decoder: { PeerReference(decoder: $0) }) as! PeerReference + self = .savedMusic(peer: peer) } } @@ -575,6 +580,9 @@ public enum PartialMediaReference: Equatable { encoder.encodeInt32(CodingCase.savedSticker.rawValue, forKey: "_r") case .recentSticker: encoder.encodeInt32(CodingCase.recentSticker.rawValue, forKey: "_r") + case let .savedMusic(peer): + encoder.encodeInt32(CodingCase.savedMusic.rawValue, forKey: "_r") + encoder.encodeObject(peer, forKey: "pg") } } @@ -592,6 +600,8 @@ public enum PartialMediaReference: Equatable { return .savedSticker(media: media) case .recentSticker: return .recentSticker(media: media) + case let .savedMusic(peer): + return .savedMusic(peer: peer, media: media) } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift index c083134b6e..881a0be6e0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift @@ -106,6 +106,15 @@ public extension PartialMediaReference { self = .savedSticker case .partialmediareferenceRecentsticker: self = .recentSticker + case .partialmediareferenceSavedmusic: + guard let value = flatBuffersObject.value(type: TelegramCore_PartialMediaReference_SavedMusic.self) else { + throw FlatBuffersError.missingRequiredField() + } + if let peer = value.peer { + self = .savedMusic(peer: try PeerReference(flatBuffersObject: peer)) + } else { + return nil + } case .none_: throw FlatBuffersError.missingRequiredField() } @@ -149,6 +158,12 @@ public extension PartialMediaReference { let start = TelegramCore_PartialMediaReference_RecentSticker.startPartialMediaReference_RecentSticker(&builder) valueType = .partialmediareferenceRecentsticker valueOffset = TelegramCore_PartialMediaReference_RecentSticker.endPartialMediaReference_RecentSticker(&builder, start: start) + case let .savedMusic(peer): + let peerOffset = peer.encodeToFlatBuffers(builder: &builder) + let start = TelegramCore_PartialMediaReference_SavedMusic.startPartialMediaReference_SavedMusic(&builder) + TelegramCore_PartialMediaReference_SavedMusic.add(peer: peerOffset, &builder) + valueType = .partialmediareferenceSavedmusic + valueOffset = TelegramCore_PartialMediaReference_SavedMusic.endPartialMediaReference_SavedMusic(&builder, start: start) } return TelegramCore_PartialMediaReference.createPartialMediaReference(&builder, valueType: valueType, valueOffset: valueOffset) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingMessageInfoAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingMessageInfoAttribute.swift index eabe75a341..6f7c4e0220 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingMessageInfoAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_OutgoingMessageInfoAttribute.swift @@ -21,13 +21,15 @@ public class OutgoingMessageInfoAttribute: MessageAttribute { public let acknowledged: Bool public let correlationId: Int64? public let bubbleUpEmojiOrStickersets: [ItemCollectionId] + public let partialReference: PartialMediaReference? - public init(uniqueId: Int64, flags: OutgoingMessageInfoFlags, acknowledged: Bool, correlationId: Int64?, bubbleUpEmojiOrStickersets: [ItemCollectionId]) { + public init(uniqueId: Int64, flags: OutgoingMessageInfoFlags, acknowledged: Bool, correlationId: Int64?, bubbleUpEmojiOrStickersets: [ItemCollectionId], partialReference: PartialMediaReference?) { self.uniqueId = uniqueId self.flags = flags self.acknowledged = acknowledged self.correlationId = correlationId self.bubbleUpEmojiOrStickersets = bubbleUpEmojiOrStickersets + self.partialReference = partialReference } required public init(decoder: PostboxDecoder) { @@ -40,6 +42,11 @@ public class OutgoingMessageInfoAttribute: MessageAttribute { } else { self.bubbleUpEmojiOrStickersets = [] } + if let partialReference = decoder.decodeAnyObjectForKey("partialReference", decoder: { PartialMediaReference(decoder: $0) }) as? PartialMediaReference { + self.partialReference = partialReference + } else { + self.partialReference = nil + } } public func encode(_ encoder: PostboxEncoder) { @@ -54,13 +61,18 @@ public class OutgoingMessageInfoAttribute: MessageAttribute { let bubbleUpEmojiOrStickersetsBuffer = WriteBuffer() ItemCollectionId.encodeArrayToBuffer(self.bubbleUpEmojiOrStickersets, buffer: bubbleUpEmojiOrStickersetsBuffer) encoder.encodeData(bubbleUpEmojiOrStickersetsBuffer.makeData(), forKey: "bubbleUpEmojiOrStickersets") + if let partialReference { + encoder.encodeObjectWithEncoder(partialReference, encoder: partialReference.encode, forKey: "partialReference") + } else { + encoder.encodeNil(forKey: "partialReference") + } } public func withUpdatedFlags(_ flags: OutgoingMessageInfoFlags) -> OutgoingMessageInfoAttribute { - return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: flags, acknowledged: self.acknowledged, correlationId: self.correlationId, bubbleUpEmojiOrStickersets: self.bubbleUpEmojiOrStickersets) + return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: flags, acknowledged: self.acknowledged, correlationId: self.correlationId, bubbleUpEmojiOrStickersets: self.bubbleUpEmojiOrStickersets, partialReference: self.partialReference) } public func withUpdatedAcknowledged(_ acknowledged: Bool) -> OutgoingMessageInfoAttribute { - return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: self.flags, acknowledged: acknowledged, correlationId: self.correlationId, bubbleUpEmojiOrStickersets: self.bubbleUpEmojiOrStickersets) + return OutgoingMessageInfoAttribute(uniqueId: self.uniqueId, flags: self.flags, acknowledged: acknowledged, correlationId: self.correlationId, bubbleUpEmojiOrStickersets: self.bubbleUpEmojiOrStickersets, partialReference: self.partialReference) } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 96323a3236..5a50ca8961 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1134,7 +1134,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) - case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _): + case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, _): if !forAdditionalServiceMessage { if let text { let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 60c67bc72d..2f5be92e7b 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -484,6 +484,7 @@ swift_library( "//submodules/TelegramUI/Components/SuggestedPostApproveAlert", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/FaceScanScreen", + "//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist", "//submodules/ContactsHelper", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 4525e1e69c..228acde43e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -274,7 +274,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy @@ -547,7 +547,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } - case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _): + case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _): if case let .generic(gift) = gift { if let releasedBy = gift.releasedBy, let peer = item.message.peers[releasedBy], let addressName = peer.addressName { creatorButtonTitle = item.presentationData.strings.Notification_StarGift_ReleasedBy("**@\(addressName)**").string diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index e5e3182af4..60857c7020 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -623,15 +623,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { |> map { resourceStatus, actualFetchStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, actualFetchStatus) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false, isSavedMusic: false) } else { updatedStatusSignal = messageFileMediaResourceStatus(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions) |> map { resourceStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false, isSavedMusic: false) } - updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: arguments.context, file: arguments.file, message: EngineMessage(arguments.message), isRecentActions: arguments.isRecentActions, isGlobalSearch: false, isDownloadList: false, isSavedMusic: false) } var isAudio = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 1ec33a7f1c..b4605b04f0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1469,7 +1469,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { playbackStatusNode.position = playbackStatusFrame.center } - let status = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(item.message), isRecentActions: item.associatedData.isRecentActions, isGlobalSearch: false, isDownloadList: false) + let status = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(item.message), isRecentActions: item.associatedData.isRecentActions, isGlobalSearch: false, isDownloadList: false, isSavedMusic: false) playbackStatusNode.status = status self.durationNode?.status = status |> map(Optional.init) diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD index ab8dc02d74..9579870f15 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 2f3eb20f16..e3da866d82 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -13,6 +13,7 @@ import PeerInfoCoverComponent import AnimatedStickerNode import TelegramAnimatedStickerNode import EmojiStatusComponent +import UIKitRuntimeUtils public final class GiftCompositionComponent: Component { public class ExternalState { @@ -24,7 +25,7 @@ public final class GiftCompositionComponent: Component { public enum Subject: Equatable { case generic(TelegramMediaFile) - case unique(StarGift.UniqueGift) + case unique([StarGift.UniqueGift.Attribute]?, StarGift.UniqueGift) case preview([StarGift.UniqueGift.Attribute]) } @@ -34,8 +35,9 @@ public final class GiftCompositionComponent: Component { let animationOffset: CGPoint? let animationScale: CGFloat? let displayAnimationStars: Bool + let revealedAttributes: Set let externalState: ExternalState? - let requestUpdate: () -> Void + let requestUpdate: (ComponentTransition) -> Void public init( context: AccountContext, @@ -44,8 +46,9 @@ public final class GiftCompositionComponent: Component { animationOffset: CGPoint? = nil, animationScale: CGFloat? = nil, displayAnimationStars: Bool = false, + revealedAttributes: Set = Set(), externalState: ExternalState? = nil, - requestUpdate: @escaping () -> Void = {} + requestUpdate: @escaping (ComponentTransition) -> Void = { _ in } ) { self.context = context self.theme = theme @@ -53,6 +56,7 @@ public final class GiftCompositionComponent: Component { self.animationOffset = animationOffset self.animationScale = animationScale self.displayAnimationStars = displayAnimationStars + self.revealedAttributes = revealedAttributes self.externalState = externalState self.requestUpdate = requestUpdate } @@ -76,6 +80,9 @@ public final class GiftCompositionComponent: Component { if lhs.displayAnimationStars != rhs.displayAnimationStars { return false } + if lhs.revealedAttributes != rhs.revealedAttributes { + return false + } return true } @@ -102,10 +109,43 @@ public final class GiftCompositionComponent: Component { private var previewBackdropIndex: Int32 = 0 private var previewPatternIndex: Int32 = 0 private var animatePreviewTransition = false + private var animateBackdropSwipe = false + private enum SpinState { case idle, spinning, decelerating, settled } + private var spinState: SpinState = .idle + private var spinLink: SharedDisplayLinkDriver.Link? + private var lastSpawnTime: CFTimeInterval? + private var lastPatternChangeTime: CFTimeInterval? + private var lastBackdropChangeTime: CFTimeInterval? + private var currentInterval: Double = 0.0 + + private var deceleraionQueue: [StarGift.UniqueGift.Attribute] = [] + private var decelerationTotalSteps: Int = 0 + private var decelerationStepIndex: Int = 0 + private var decelContainer: UIView? + private var decelItemHosts: [UIView] = [] + private let decelAnimationKey = "decel.container.move" + + private var activeWrappers: [UIView] = [] + + private struct SpinGeom { + var availableSize: CGSize + var iconSize: CGSize + var scale: CGFloat + var centerX: CGFloat + var centerY: CGFloat + } + private var spinGeom: SpinGeom? + + private var spinPool: [StarGift.UniqueGift.Attribute] = [] + private var spinPoolIndex: Int = 0 + + private let baseAnimDuration: Double = 0.4 + private let maxAnimDuration: Double = 1.3 + private let spacingX: CGFloat = 50.0 + override init(frame: CGRect) { super.init(frame: frame) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) } @@ -115,17 +155,376 @@ public final class GiftCompositionComponent: Component { deinit { self.disposables.dispose() + self.previewTimer?.invalidate() } @objc private func handleTap() { - guard let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl else { - return - } + guard let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl else { return } if case .once = animationNode.playbackMode, !animationNode.isPlaying { animationNode.playOnce() } } + private func stopSpinIfNeeded() { + self.spinState = .idle + self.spinLink?.invalidate() + self.spinLink = nil + self.lastSpawnTime = nil + self.currentInterval = 0.0 + self.deceleraionQueue.removeAll() + self.decelerationTotalSteps = 0 + self.decelerationStepIndex = 0 + self.spinPool.removeAll() + self.spinPoolIndex = 0 + self.spinGeom = nil + + for wrapper in self.activeWrappers { + wrapper.layer.removeAllAnimations() + wrapper.removeFromSuperview() + } + self.activeWrappers.removeAll() + + if let c = self.decelContainer { + c.layer.removeAllAnimations() + c.removeFromSuperview() + } + self.decelContainer = nil + self.decelItemHosts.removeAll() + } + + private func ensureDisplayLink() { + if self.spinLink != nil { return } + self.spinLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + self?.tick() + }) + } + + private func spawnModelItem( + _ attribute: StarGift.UniqueGift.Attribute, + animDuration: Double + ) { + guard let geom = self.spinGeom, case let .model(_, file, _) = attribute else { + return + } + + let node = DefaultAnimatedStickerNodeImpl() + node.isUserInteractionEnabled = false + let pathPrefix = self.component!.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + node.setup( + source: AnimatedStickerResourceSource(account: self.component!.context.account, resource: file.resource, isVideo: file.isVideoSticker), + width: Int(geom.iconSize.width * 1.6), + height: Int(geom.iconSize.height * 1.6), + playbackMode: .still(.start), + mode: .direct(cachePathPrefix: pathPrefix) + ) + node.updateLayout(size: geom.iconSize) + + let scaleValue = geom.scale + let visualSize = CGSize(width: geom.iconSize.width * scaleValue, height: geom.iconSize.height * scaleValue) + let wrapper = UIView(frame: CGRect(origin: .zero, size: visualSize)) + wrapper.clipsToBounds = false + + let host = node.view + host.frame = CGRect(origin: .zero, size: geom.iconSize) + host.layer.bounds = CGRect(origin: .zero, size: geom.iconSize) + host.layer.position = CGPoint(x: geom.iconSize.width / 2.0, y: geom.iconSize.height / 2.0) + host.layer.transform = CATransform3DMakeScale(scaleValue, scaleValue, 1.0) + wrapper.addSubview(host) + + self.addSubview(wrapper) + self.activeWrappers.append(wrapper) + + let centerY = geom.centerY - visualSize.height / 2.0 + let startX = -visualSize.width * 1.5 + let endX = geom.availableSize.width + visualSize.width + + wrapper.frame.origin = CGPoint(x: endX, y: centerY) + + let travelDistance = abs(startX - endX) + let pitch = visualSize.width + self.spacingX + + wrapper.layer.animatePosition( + from: CGPoint(x: -travelDistance, y: 0.0), + to: .zero, + duration: animDuration, + timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, + additive: true, + completion: { [weak self, weak wrapper] _ in + if let self, let w = wrapper { + self.activeWrappers.removeAll { $0 === w } + w.removeFromSuperview() + } + } + ) + + self.currentInterval = Double(pitch / travelDistance) * animDuration * 0.6 + } + + private func finishSettled() { + guard self.spinState != .settled else { return } + self.spinState = .settled + self.spinLink?.invalidate() + self.spinLink = nil + + for v in self.activeWrappers { + v.layer.removeAllAnimations() + v.removeFromSuperview() + } + self.activeWrappers.removeAll() + } + + private func startSpinningUnique( + availableSize: CGSize, + iconSize: CGSize, + scale: CGFloat, + pool: [StarGift.UniqueGift.Attribute] + ) { + self.stopSpinIfNeeded() + + self.spinPool = pool + self.spinPoolIndex = 0 + let centerY = 88.0 + (self.component?.animationOffset?.y ?? 0.0) + + self.spinGeom = SpinGeom( + availableSize: availableSize, + iconSize: iconSize, + scale: scale, + centerX: availableSize.width / 2.0 + (self.component?.animationOffset?.x ?? 0.0), + centerY: centerY + ) + + self.spinState = .spinning + self.lastSpawnTime = nil + self.currentInterval = 0 + self.ensureDisplayLink() + } + + private func beginDecelerationWithQueue( + tail: [StarGift.UniqueGift.Attribute], + availableSize: CGSize, + iconSize: CGSize, + scale: CGFloat + ) { + guard let geom = self.spinGeom, !tail.isEmpty else { return } + + // Build container + let visualSize = CGSize(width: iconSize.width * scale, height: iconSize.height * scale) + let pitch = visualSize.width + self.spacingX + let count = tail.count + let containerWidth = CGFloat(count) * visualSize.width + CGFloat(max(count - 1, 0)) * self.spacingX + let containerHeight = visualSize.height + + let container = UIView(frame: CGRect(origin: .zero, size: CGSize(width: containerWidth, height: containerHeight))) + container.isUserInteractionEnabled = false + container.clipsToBounds = false + + // Y is fixed; we’ll animate X only + let containerY = geom.centerY - containerHeight / 2.0 + container.frame.origin.y = containerY + self.addSubview(container) + self.decelContainer = container + self.decelItemHosts.removeAll() + + // Fill container with hosts at fixed pitch + for (i, attribute) in tail.reversed().enumerated() { + guard case let .model(_, file, _) = attribute else { continue } + + // Node + let node = DefaultAnimatedStickerNodeImpl() + node.isUserInteractionEnabled = false + let pathPrefix = self.component!.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + node.setup( + source: AnimatedStickerResourceSource(account: self.component!.context.account, resource: file.resource, isVideo: file.isVideoSticker), + width: Int(iconSize.width * 1.6), + height: Int(iconSize.height * 1.6), + playbackMode: .still(.start), + mode: .direct(cachePathPrefix: pathPrefix) + ) + node.updateLayout(size: iconSize) + node.visibility = true + if i < 4 { + node.playOnce(); + } + + let host = node.view + host.bounds = CGRect(origin: .zero, size: iconSize) + host.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) + + let hostView = UIView(frame: CGRect(origin: CGPoint(x: CGFloat(i) * pitch, y: 0), size: visualSize)) + host.center = CGPoint(x: visualSize.width / 2.0, y: visualSize.height / 2.0) + hostView.addSubview(host) + container.addSubview(hostView) + self.decelItemHosts.append(hostView) + + if i == 0 { + self.animationNode = node + + let factors: [CGFloat] = [1.0, 1.3, 0.92, 1.18, 0.98, 1.0] + let values = factors.map { NSNumber(value: Double($0)) } + let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale") + scaleAnim.beginTime = CACurrentMediaTime() + 0.6 + scaleAnim.values = values + scaleAnim.keyTimes = [0.0, 0.35, 0.55, 0.75, 0.9, 1.0].map(NSNumber.init) + scaleAnim.timingFunctions = [ + CAMediaTimingFunction(name: .easeOut), + CAMediaTimingFunction(name: .easeIn), + CAMediaTimingFunction(name: .easeOut), + CAMediaTimingFunction(name: .easeIn), + CAMediaTimingFunction(name: .easeOut) + ] + scaleAnim.duration = 0.85 + scaleAnim.isRemovedOnCompletion = true + host.layer.add(scaleAnim, forKey: "bounce") + } + + if i == 1 { + hostView.alpha = 0.0 + hostView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.7) + } + } + + container.frame.origin.x = floor((availableSize.width - visualSize.width) / 2.0) + + container.layer.animatePosition( + from: CGPoint(x: -containerWidth - visualSize.width * 0.5 + containerWidth / 2.0 - self.spacingX - 70.0, y: container.frame.center.y), + to: CGPoint(x: container.frame.center.x, y: container.frame.center.y), + duration: self.maxAnimDuration, + timingFunction: kCAMediaTimingFunctionSpring, + completion: { [weak self] _ in + guard let self, let container = self.decelContainer else { + return + } + let _ = container + //self.handleDecelArrived(container: container, iconSize: iconSize, visualSize: visualSize) + } + ) + + self.spinState = .decelerating + self.spinLink?.invalidate() + self.spinLink = nil + } + + private func handleDecelArrived(container: UIView, iconSize: CGSize, visualSize: CGSize) { + let isFinalIndex = self.decelItemHosts.count - 1 + guard isFinalIndex >= 0, let finalHostView = self.decelItemHosts.last else { return } + + // Prepare final node + guard let node = self.animationNode as? DefaultAnimatedStickerNodeImpl else { return } + node.playbackMode = .once + node.visibility = true + + // Convert final host center into self coords + let finalCenterInSelf = container.convert(finalHostView.center, to: self) + + // Place node directly in self, exactly where the final host is + let host = node.view + if host.superview !== self { self.addSubview(host) } + node.updateLayout(size: iconSize) + host.bounds = CGRect(origin: .zero, size: iconSize) + host.layer.position = finalCenterInSelf + + // Remove container + container.removeFromSuperview() + self.decelContainer = nil + self.decelItemHosts.removeAll() + + // Bounce scale (same values you already used) + let factors: [CGFloat] = [1.0, 1.3, 0.92, 1.18, 0.98, 1.0] + let values = factors.map { NSNumber(value: Double($0)) } + let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale") + scaleAnim.values = values + scaleAnim.keyTimes = [0.0, 0.35, 0.55, 0.75, 0.9, 1.0].map(NSNumber.init) + scaleAnim.timingFunctions = [ + CAMediaTimingFunction(name: .easeOut), + CAMediaTimingFunction(name: .easeIn), + CAMediaTimingFunction(name: .easeOut), + CAMediaTimingFunction(name: .easeIn), + CAMediaTimingFunction(name: .easeOut) + ] + scaleAnim.duration = 0.85 + scaleAnim.isRemovedOnCompletion = true + host.layer.add(scaleAnim, forKey: "bounce") + + // Play the “once” sticker & finish + node.playOnce() + self.finishSettled() + } + + private func tick() { + guard let component = self.component else { return } + let now = CACurrentMediaTime() + + switch self.spinState { + case .spinning: + if self.lastSpawnTime == nil || now - (self.lastSpawnTime ?? now) >= self.currentInterval { + self.lastSpawnTime = now + + guard !self.spinPool.isEmpty else { return } + + if self.spinPoolIndex >= self.spinPool.count { self.spinPoolIndex = 0 } + let next = self.spinPool[self.spinPoolIndex] + self.spinPoolIndex += 1 + + self.spawnModelItem(next, animDuration: self.baseAnimDuration) + } + + var updateNeeded = false + if self.lastPatternChangeTime == nil || now - (self.lastPatternChangeTime ?? now) >= self.currentInterval * 6.0 { + self.lastPatternChangeTime = now + + if component.revealedAttributes.contains(.pattern) { + if self.previewPatternIndex != -1 { + self.previewPatternIndex = -1 + self.animatePreviewTransition = true + updateNeeded = true + } + } else { + let previousPatternIndex = self.previewPatternIndex + var randomPatternIndex = previousPatternIndex + while randomPatternIndex == previousPatternIndex && !self.previewPatterns.isEmpty { + randomPatternIndex = Int32.random(in: 0 ..< Int32(self.previewPatterns.count)) + } + if !self.previewPatterns.isEmpty { self.previewPatternIndex = randomPatternIndex } + + self.animatePreviewTransition = true + updateNeeded = true + } + } + if self.lastBackdropChangeTime == nil || now - (self.lastBackdropChangeTime ?? now) >= self.currentInterval * 3.55 { + self.lastBackdropChangeTime = now + + if component.revealedAttributes.contains(.backdrop) { + if self.previewBackdropIndex != -1 { + self.previewBackdropIndex = -1 + self.animateBackdropSwipe = true + updateNeeded = true + } + } else { + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty { + randomBackdropIndex = Int32.random(in: 0 ..< Int32(self.previewBackdrops.count)) + } + if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex } + + self.animateBackdropSwipe = true + updateNeeded = true + } + } + + if updateNeeded { + self.componentState?.updated(transition: .easeInOut(duration: 0.25)) + self.component?.requestUpdate(.easeInOut(duration: 0.25)) + } + case .decelerating: + break + case .idle, .settled: + self.spinLink?.invalidate() + self.spinLink = nil + } + } + + public func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component @@ -140,21 +539,30 @@ public final class GiftCompositionComponent: Component { var files: [Int64: TelegramMediaFile] = [:] var loop = false + + var uniqueSpinContext: (previewAttributes: [StarGift.UniqueGift.Attribute], mainGift: StarGift.UniqueGift)? = nil + switch component.subject { case let .generic(file): animationFile = file self.currentFile = file + self.stopSpinIfNeeded() if let previewTimer = self.previewTimer { previewTimer.invalidate() self.previewTimer = nil } - if !self.fetchedFiles.contains(file.fileId.id) { self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) self.fetchedFiles.insert(file.fileId.id) } - case let .unique(gift): + + case let .unique(previewAttributesOpt, gift): + if let previewTimer = self.previewTimer { + previewTimer.invalidate() + self.previewTimer = nil + } + for attribute in gift.attributes { switch attribute { case let .model(_, file, _): @@ -174,13 +582,23 @@ public final class GiftCompositionComponent: Component { break } } - - if let previewTimer = self.previewTimer { - previewTimer.invalidate() - self.previewTimer = nil + if let previewAttributes = previewAttributesOpt, !previewAttributes.isEmpty { + if self.previewPatternIndex != -1, case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] { + patternFile = file + files[file.fileId.id] = file + } + if self.previewBackdropIndex != -1, case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] { + backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) + secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) + patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) + } + uniqueSpinContext = (previewAttributes, gift) + } else { + self.stopSpinIfNeeded() } case let .preview(sampleAttributes): loop = true + self.stopSpinIfNeeded() if self.previewModels.isEmpty { var models: [StarGift.UniqueGift.Attribute] = [] @@ -188,14 +606,10 @@ public final class GiftCompositionComponent: Component { var backdrops: [StarGift.UniqueGift.Attribute] = [] for attribute in sampleAttributes { switch attribute { - case .model: - models.append(attribute) - case .pattern: - patterns.append(attribute) - case .backdrop: - backdrops.append(attribute) - default: - break + case .model: models.append(attribute) + case .pattern: patterns.append(attribute) + case .backdrop: backdrops.append(attribute) + default: break } } self.previewModels = models @@ -203,67 +617,72 @@ public final class GiftCompositionComponent: Component { self.previewBackdrops = backdrops } - for case let .model(_, file, _) in self.previewModels { - if !self.fetchedFiles.contains(file.fileId.id) { - self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) - self.fetchedFiles.insert(file.fileId.id) - } + for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + self.fetchedFiles.insert(file.fileId.id) } - for case let .pattern(_, file, _) in self.previewModels { - if !self.fetchedFiles.contains(file.fileId.id) { - self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) - self.fetchedFiles.insert(file.fileId.id) - } + for case let .pattern(_, file, _) in self.previewPatterns where !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + self.fetchedFiles.insert(file.fileId.id) } if !self.previewModels.isEmpty { + if self.previewPatternIndex < 0 { + self.previewPatternIndex = 0 + } + if self.previewBackdropIndex < 0 { + self.previewBackdropIndex = 0 + } if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] { animationFile = file } - if case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] { patternFile = file files[file.fileId.id] = file } - if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] { backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) } } - + if self.previewTimer == nil { self.previewTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in - guard let self, !self.previewModels.isEmpty else { - return - } + guard let self, !self.previewModels.isEmpty else { return } self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count) let previousPatternIndex = self.previewPatternIndex var randomPatternIndex = previousPatternIndex - while randomPatternIndex == previousPatternIndex { + while randomPatternIndex == previousPatternIndex && !self.previewPatterns.isEmpty { randomPatternIndex = Int32.random(in: 0 ..< Int32(self.previewPatterns.count)) } - self.previewPatternIndex = randomPatternIndex + if !self.previewPatterns.isEmpty { self.previewPatternIndex = randomPatternIndex } let previousBackdropIndex = self.previewBackdropIndex var randomBackdropIndex = previousBackdropIndex - while randomBackdropIndex == previousBackdropIndex { + while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty { randomBackdropIndex = Int32.random(in: 0 ..< Int32(self.previewBackdrops.count)) } - self.previewBackdropIndex = randomBackdropIndex + if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex } self.animatePreviewTransition = true self.componentState?.updated(transition: .easeInOut(duration: 0.25)) + self.component?.requestUpdate(.easeInOut(duration: 0.25)) }, queue: Queue.mainQueue()) self.previewTimer?.start() } } component.externalState?.previewPatternColor = secondBackgroundColor - + + var animateBackdropSwipe = false + if self.animateBackdropSwipe { + animateBackdropSwipe = true + self.animateBackdropSwipe = false + } + var animateTransition = false if self.animatePreviewTransition { animateTransition = true @@ -278,16 +697,24 @@ public final class GiftCompositionComponent: Component { if let backgroundColor { var backgroundTransition = transition - - if animateTransition, let backgroundView = self.background.view as? PeerInfoCoverComponent.View { - backgroundView.animateTransition() + if let backgroundView = self.background.view as? PeerInfoCoverComponent.View { + if animateTransition { + var bounce = true + var background = true + if case .unique = component.subject { + bounce = self.previewPatternIndex == -1 + background = false + } + backgroundView.animateTransition(background: background, bounce: bounce) + } + if animateBackdropSwipe { + backgroundView.animateSwipeTransition() + } } - var avatarCenter = CGPoint(x: availableSize.width / 2.0, y: 104.0) if let _ = component.animationScale { avatarCenter = CGPoint(x: avatarCenter.x, y: 67.0) } - let _ = self.background.update( transition: backgroundTransition, component: AnyComponent(PeerInfoCoverComponent( @@ -311,7 +738,6 @@ public final class GiftCompositionComponent: Component { backgroundView.clipsToBounds = true backgroundView.isUserInteractionEnabled = false self.insertSubview(backgroundView, at: 0) - if previousComponent != nil { backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } @@ -323,9 +749,99 @@ public final class GiftCompositionComponent: Component { backgroundView.removeFromSuperview() }) } - + let iconSize = CGSize(width: 136.0, height: 136.0) + if let (previewAttributes, mainGift) = uniqueSpinContext { + var mainModelFile: TelegramMediaFile? + for attribute in mainGift.attributes { + if case let .model(_, file, _) = attribute { mainModelFile = file; break } + } + + var models: [StarGift.UniqueGift.Attribute] = [] + for attribute in previewAttributes { + if case let .model(_, file, _) = attribute, + file.fileId.id != mainModelFile?.fileId.id { + models.append(attribute) + } + } + + if models.isEmpty, let _ = mainModelFile { + return availableSize + } + + for case let .model(_, file, _) in models where !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched( + account: component.context.account, + userLocation: .other, + fileReference: .standalone(media: file), + resource: file.resource + ).start()) + self.fetchedFiles.insert(file.fileId.id) + } + if let mainModelFile, !self.fetchedFiles.contains(mainModelFile.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched( + account: component.context.account, + userLocation: .other, + fileReference: .standalone(media: mainModelFile), + resource: mainModelFile.resource + ).start()) + self.fetchedFiles.insert(mainModelFile.fileId.id) + } + + let wasAnimatingModel = previousComponent != nil && !(previousComponent!.revealedAttributes.contains(.model)) + let isAnimatingModel = !component.revealedAttributes.contains(.model) + + let wasAnimating = wasAnimatingModel + let nowAnimating = isAnimatingModel + + if nowAnimating { + if let disappearing = self.animationNode { + self.animationNode = nil + disappearing.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { _ in + disappearing.view.removeFromSuperview() + }) + } + } + + let scaleValue: CGFloat = component.animationScale ?? 1.0 + + if nowAnimating && (!wasAnimating || self.spinState != .spinning) { + self.startSpinningUnique( + availableSize: availableSize, + iconSize: iconSize, + scale: scaleValue, + pool: models + ) + } else if !nowAnimating && wasAnimating { + var tail = Array(models.shuffled().prefix(6)) + if let mainModelFile { + tail.append(.model(name: "", file: mainModelFile, rarity: 0)) + } + self.beginDecelerationWithQueue( + tail: tail, + availableSize: availableSize, + iconSize: iconSize, + scale: scaleValue + ) + } else if self.spinState == .spinning { + let centerY = 88.0 + (component.animationOffset?.y ?? 0.0) + self.spinGeom = SpinGeom( + availableSize: availableSize, + iconSize: iconSize, + scale: scaleValue, + centerX: availableSize.width / 2.0 + (component.animationOffset?.x ?? 0.0), + centerY: centerY + ) + } + + return availableSize + } + + if self.spinState != .idle && self.spinState != .settled { + self.stopSpinIfNeeded() + } + var startFromIndex: Int? var animationTransition = transition if animateTransition, let disappearingAnimationNode = self.animationNode { @@ -337,46 +853,39 @@ public final class GiftCompositionComponent: Component { animationTransition = .immediate } - if let file = animationFile { - let animationNode: AnimatedStickerNode - if self.animationNode == nil { - animationTransition = .immediate - animationNode = DefaultAnimatedStickerNodeImpl() - animationNode.isUserInteractionEnabled = false - self.animationNode = animationNode - - self.addSubview(animationNode.view) - - let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) - animationNode.setup(source: AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(iconSize.width * 1.6), height: Int(iconSize.height * 1.6), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) - - if let startFromIndex { - if let animationNode = animationNode as? DefaultAnimatedStickerNodeImpl { - animationNode.playbackMode = loop ? .loop : .once - } - animationNode.play(firstFrame: false, fromIndex: startFromIndex) - } else { - if loop { - animationNode.playLoop() - } else { - animationNode.playOnce() - } - } - animationNode.visibility = true - animationNode.updateLayout(size: iconSize) - - if animateTransition { - animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - } + if let file = animationFile, self.animationNode == nil { + animationTransition = .immediate + let node = DefaultAnimatedStickerNodeImpl() + node.isUserInteractionEnabled = false + self.animationNode = node + self.addSubview(node.view) + let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + node.setup(source: AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker), + width: Int(iconSize.width * 1.6), height: Int(iconSize.height * 1.6), + playbackMode: loop ? .loop : .once, + mode: .direct(cachePathPrefix: pathPrefix)) + if let startFromIndex { + node.play(firstFrame: false, fromIndex: startFromIndex) + } else { + if loop { node.playLoop() } else { node.playOnce() } + } + node.visibility = true + node.updateLayout(size: iconSize) + if animateTransition { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } + if let animationNode = self.animationNode { let offset = component.animationOffset ?? .zero var size = CGSize(width: iconSize.width, height: iconSize.height) if let scale = component.animationScale { size = CGSize(width: size.width * scale, height: size.height * scale) } - let animationFrame = CGRect(origin: CGPoint(x: availableSize.width / 2.0 + offset.x - size.width / 2.0, y: 88.0 + offset.y - size.height / 2.0), size: size) + let animationFrame = CGRect( + origin: CGPoint(x: availableSize.width / 2.0 + offset.x - size.width / 2.0, y: 88.0 + offset.y - size.height / 2.0), + size: size + ) animationNode.layer.bounds = CGRect(origin: .zero, size: iconSize) animationTransition.setPosition(layer: animationNode.layer, position: animationFrame.center) animationTransition.setScale(layer: animationNode.layer, scale: size.width / iconSize.width) @@ -405,7 +914,7 @@ public final class GiftCompositionComponent: Component { }) } } - + return availableSize } } @@ -424,7 +933,6 @@ private final class StarsEffectLayer: SimpleLayer { override init() { super.init() - self.addSublayer(self.emitterLayer) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 6881c00ac3..901d9dfd33 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -154,6 +154,7 @@ public final class GiftItemComponent: Component { let isSelected: Bool let isPinned: Bool let isEditing: Bool + let isDateLocked: Bool let mode: Mode let action: (() -> Void)? let contextAction: ((UIView, ContextGesture) -> Void)? @@ -176,6 +177,7 @@ public final class GiftItemComponent: Component { isSelected: Bool = false, isPinned: Bool = false, isEditing: Bool = false, + isDateLocked: Bool = false, mode: Mode = .generic, action: (() -> Void)? = nil, contextAction: ((UIView, ContextGesture) -> Void)? = nil @@ -197,6 +199,7 @@ public final class GiftItemComponent: Component { self.isSelected = isSelected self.isPinned = isPinned self.isEditing = isEditing + self.isDateLocked = isDateLocked self.mode = mode self.action = action self.contextAction = contextAction @@ -254,6 +257,9 @@ public final class GiftItemComponent: Component { if lhs.isEditing != rhs.isEditing { return false } + if lhs.isDateLocked != rhs.isDateLocked { + return false + } if lhs.mode != rhs.mode { return false } @@ -298,6 +304,7 @@ public final class GiftItemComponent: Component { private var iconBackground: UIVisualEffectView? private var hiddenIcon: UIImageView? private var pinnedIcon: UIImageView? + private var dateLockedIcon: UIImageView? private var resellBackground: BlurredBackgroundView? private let reselLabel = ComponentView() @@ -902,6 +909,22 @@ public final class GiftItemComponent: Component { }) pinnedIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } + + if component.isDateLocked { + let dateLockedIcon: UIImageView + if let currentIcon = self.dateLockedIcon { + dateLockedIcon = currentIcon + } else { + dateLockedIcon = UIImageView(image: UIImage(bundleImageName: "Peer Info/DateLockedIcon")?.withRenderingMode(.alwaysTemplate)) + self.dateLockedIcon = dateLockedIcon + self.addSubview(dateLockedIcon) + } + dateLockedIcon.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 24.0, height: 24.0)) + dateLockedIcon.tintColor = component.theme.list.itemDestructiveColor + } else if let dateLockedIcon = self.dateLockedIcon { + self.dateLockedIcon = nil + dateLockedIcon.removeFromSuperview() + } if component.isHidden && !component.isEditing { let hiddenIcon: UIImageView diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 90a8a1ae83..6f1baf3c9e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -282,6 +282,7 @@ final class GiftOptionsScreenComponent: Component { let availableWidth = self.scrollView.bounds.width let contentOffset = self.scrollView.contentOffset.y + let strings = environment.strings let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0 if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view { @@ -414,6 +415,7 @@ final class GiftOptionsScreenComponent: Component { ) } + var isDateLocked = false let subject: GiftItemComponent.Subject switch gift { case let .generic(gift): @@ -427,6 +429,7 @@ final class GiftOptionsScreenComponent: Component { } else { subject = .starGift(gift: gift, price: "# \(presentationStringsFormattedNumber(Int32(gift.price), environment.dateTimeFormat.groupingSeparator))") } + isDateLocked = gift.lockedUntilDate != nil case let .unique(gift): subject = .uniqueGift(gift: gift, price: nil) } @@ -444,7 +447,8 @@ final class GiftOptionsScreenComponent: Component { subject: subject, ribbon: ribbon, outline: outline, - isSoldOut: isSoldOut + isSoldOut: isSoldOut, + isDateLocked: isDateLocked ) ), effectAlignment: .center, @@ -458,6 +462,31 @@ final class GiftOptionsScreenComponent: Component { mainController = controller } if case let .generic(gift) = gift { + if isDateLocked { + let _ = (component.context.engine.payments.checkCanSendStarGift(giftId: gift.id) + |> deliverOnMainQueue).start(next: { [weak controller] result in + guard let controller else { + return + } + if case let .unavailable(text, entities) = result { + let theme = AlertControllerTheme(presentationData: component.context.sharedContext.currentPresentationData.with { $0 }) + let font = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) + let boldFont = Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) + let attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: theme.primaryColor, linkColor: .black, baseFont: font, linkFont: font, boldFont: boldFont, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: nil, paragraphAlignment: .center) + + var dismissImpl: (() -> Void)? + let alertController = textAlertController(theme: theme, title: NSAttributedString(string: strings.Gift_Options_GiftLocked_Title, font: Font.semibold(theme.baseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center), text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: { + dismissImpl?() + })], actionLayout: .horizontal, dismissOnOutsideTap: true) + dismissImpl = { [weak alertController] in + alertController?.dismissAnimated() + } + controller.present(alertController, in: .window(.root)) + } + }) + return + } + if let perUserLimit = gift.perUserLimit, perUserLimit.remains == 0 { let text = environment.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit.total) let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 8045042ac1..1606a4ee30 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -239,7 +239,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { case let .starGift(gift): media = [ TelegramMediaAction( - action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil) + action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index e976012283..b7c8c81f79 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -752,6 +752,10 @@ final class GiftSetupScreenComponent: Component { } else if isChannelGift { navigationTitleString = environment.strings.Gift_SendChannel_Title } else { + var peerName = peerName + if peerName.count > 22 { + peerName = "\(peerName.prefix(22))…" + } navigationTitleString = environment.strings.Gift_Send_TitleTo(peerName).string } let navigationTitleSize = self.navigationTitle.update( diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index a2e66c96b7..9e0aca1b52 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -53,6 +53,7 @@ swift_library( "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", + "//submodules/ImageBlur", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index 775f7a183f..9bc71a8e98 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -276,7 +276,7 @@ private final class GiftValueSheetContent: CombinedComponent { animationScale: nil, displayAnimationStars: false, externalState: giftCompositionExternalState, - requestUpdate: { [weak state] in + requestUpdate: { [weak state] _ in state?.updated() } ), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 503bdb7141..8894382ef9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -28,7 +28,6 @@ import ConfettiEffect import PlainButtonComponent import CheckComponent import TooltipUI -import GiftAnimationComponent import LottieComponent import ContextUI import TelegramNotices @@ -36,6 +35,7 @@ import PremiumLockButtonSubtitleComponent import StarsBalanceOverlayComponent import BalanceNeededScreen import GiftItemComponent +import GiftAnimationComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -75,6 +75,9 @@ private final class GiftViewSheetContent: CombinedComponent { private let context: AccountContext private(set) var subject: GiftViewScreen.Subject + var justUpgraded = false + var revealedAttributes = Set() + var revealedNumberDigits: Int = 0 private let getController: () -> ViewController? @@ -102,6 +105,8 @@ private final class GiftViewSheetContent: CombinedComponent { var inProgress = false + var testUpgradeAnimation = !"".isEmpty + var nextGiftToUpgrade: ProfileGiftsContext.State.StarGift? var inUpgradePreview = false var upgradeForm: BotPaymentForm? @@ -208,6 +213,31 @@ private final class GiftViewSheetContent: CombinedComponent { } }) } + + if self.testUpgradeAnimation { + if gift.giftId != 0 { + self.sampleDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.giftId) + |> deliverOnMainQueue).start(next: { [weak self] attributes in + guard let self else { + return + } + self.sampleGiftAttributes = attributes + + for attribute in attributes { + switch attribute { + case let .model(_, file, _): + self.sampleDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + case let .pattern(_, file, _): + self.sampleDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + default: + break + } + } + + self.updated() + })) + } + } } else if case let .generic(gift) = arguments.gift { if let releasedBy = gift.releasedBy { peerIds.append(releasedBy) @@ -1496,6 +1526,53 @@ private final class GiftViewSheetContent: CombinedComponent { } func commitUpgrade() { + if self.testUpgradeAnimation, let arguments = self.subject.arguments, case let .unique(uniqueGift) = arguments.gift { + self.inProgress = true + self.updated() + + if let controller = self.getController() as? GiftViewScreen { + controller.showBalance = false + } + + Queue.mainQueue().after(0.5, { + self.inUpgradePreview = false + self.inProgress = false + + self.justUpgraded = true + self.revealedNumberDigits = -1 + + for i in 0 ..< "\(uniqueGift.number)".count { + Queue.mainQueue().after(0.2 + Double(i) * 0.3) { + self.revealedNumberDigits += 1 + self.updated(transition: .immediate) + } + } + self.updated(transition: .spring(duration: 0.4)) + + Queue.mainQueue().after(1.5) { + self.revealedAttributes.insert(.backdrop) + self.updated(transition: .immediate) + + Queue.mainQueue().after(1.0) { + self.revealedAttributes.insert(.pattern) + self.updated(transition: .immediate) + + Queue.mainQueue().after(1.0) { + self.revealedAttributes.insert(.model) + self.updated(transition: .immediate) + + Queue.mainQueue().after(0.6) { + if let controller = self.getController() as? GiftViewScreen { + controller.animateSuccess() + } + } + } + } + } + }) + return + } + guard let arguments = self.subject.arguments, let peerId = arguments.peerId, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { return } @@ -1547,6 +1624,7 @@ private final class GiftViewSheetContent: CombinedComponent { self.nextGiftToUpgrade = controller.nextUpgradableGift } + self.justUpgraded = true self.subject = .profileGift(peerId, result) controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) @@ -1708,6 +1786,7 @@ private final class GiftViewSheetContent: CombinedComponent { let descriptionButton = Child(PlainButtonComponent.self) let description = Child(MultilineTextComponent.self) + let animatedDescription = Child(HStack.self) let transferButton = Child(PlainButtonComponent.self) let wearButton = Child(PlainButtonComponent.self) @@ -1888,6 +1967,11 @@ private final class GiftViewSheetContent: CombinedComponent { } }, morePressed: { [weak state] node, gesture in + if state?.testUpgradeAnimation == true { + state?.requestUpgradePreview() + return + } + state?.openMore(node: node, gesture: gesture) } ), @@ -1899,7 +1983,7 @@ private final class GiftViewSheetContent: CombinedComponent { let headerHeight: CGFloat let headerSubject: GiftCompositionComponent.Subject? - if let uniqueGift { + if let uniqueGift, !state.inUpgradePreview { if showWearPreview { headerHeight = 200.0 } else if case let .peerId(peerId) = uniqueGift.owner, peerId == component.context.account.peerId || isChannelGift { @@ -1907,7 +1991,7 @@ private final class GiftViewSheetContent: CombinedComponent { } else { headerHeight = 240.0 } - headerSubject = .unique(uniqueGift) + headerSubject = .unique(state.justUpgraded ? state.sampleGiftAttributes : nil, uniqueGift) } else if state.inUpgradePreview, let attributes = state.sampleGiftAttributes { headerHeight = 258.0 headerSubject = .preview(attributes) @@ -2009,6 +2093,8 @@ private final class GiftViewSheetContent: CombinedComponent { animationScale = 0.19 } + var headerComponents: [() -> Void] = [] + if let headerSubject { let animation = animation.update( component: GiftCompositionComponent( @@ -2018,17 +2104,20 @@ private final class GiftViewSheetContent: CombinedComponent { animationOffset: animationOffset, animationScale: animationScale, displayAnimationStars: showWearPreview, + revealedAttributes: state.revealedAttributes, externalState: giftCompositionExternalState, - requestUpdate: { [weak state] in - state?.updated() + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) } ), availableSize: CGSize(width: context.availableSize.width, height: headerHeight), transition: context.transition ) - context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) - ) + headerComponents.append({ + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) + ) + }) } originY += headerHeight @@ -2051,11 +2140,13 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: 100.0, height: 100.0), transition: context.transition ) - context.add(wearAvatar - .position(CGPoint(x: context.availableSize.width / 2.0, y: 67.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) + headerComponents.append({ + context.add(wearAvatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: 67.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + }) } let wearPeerStatus = wearPeerStatus.update( @@ -2074,21 +2165,19 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .immediate ) - context.add(wearPeerNameChild - .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 144.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - context.add(wearPeerStatus - .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - originY += 18.0 - originY += 28.0 - originY += 18.0 - originY += 20.0 - originY += 24.0 + headerComponents.append({ + context.add(wearPeerNameChild + .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 144.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + context.add(wearPeerStatus + .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) + originY += 108.0 let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor @@ -2144,11 +2233,13 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0), transition: context.transition ) - context.add(wearPerks - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) + headerComponents.append({ + context.add(wearPerks + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) originY += wearPerks.size.height originY += 16.0 } else if showUpgradePreview { @@ -2158,15 +2249,22 @@ private final class GiftViewSheetContent: CombinedComponent { let transferableText: String let tradableText: String if !incoming, case let .profileGift(peerId, _) = subject, let peer = state.peerMap[peerId] { - let name = peer.compactDisplayTitle + var peerName = peer.compactDisplayTitle + if peerName.count > 22 { + peerName = "\(peerName.prefix(22))…" + } title = environment.strings.Gift_Upgrade_GiftTitle - description = environment.strings.Gift_Upgrade_GiftDescription(name).string - uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(name).string - transferableText = strings.Gift_Upgrade_Transferable_GiftDescription(name).string - tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(name).string - } else if case let .upgradePreview(_, name) = component.subject { + description = environment.strings.Gift_Upgrade_GiftDescription(peerName).string + uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(peerName).string + transferableText = strings.Gift_Upgrade_Transferable_GiftDescription(peerName).string + tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(peerName).string + } else if case let .upgradePreview(_, peerName) = component.subject { + var peerName = peerName + if peerName.count > 22 { + peerName = "\(peerName.prefix(22))…" + } title = environment.strings.Gift_Upgrade_IncludeTitle - description = environment.strings.Gift_Upgrade_IncludeDescription(name).string + description = environment.strings.Gift_Upgrade_IncludeDescription(peerName).string uniqueText = strings.Gift_Upgrade_Unique_IncludeDescription transferableText = strings.Gift_Upgrade_Transferable_IncludeDescription tradableText = strings.Gift_Upgrade_Tradable_IncludeDescription @@ -2197,31 +2295,34 @@ private final class GiftViewSheetContent: CombinedComponent { text: .plain(NSAttributedString( string: description, font: Font.regular(13.0), - textColor: vibrantColor, + textColor: .white, paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 5, - lineSpacing: 0.2 + lineSpacing: 0.2, + tintColor: vibrantColor ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate + transition: context.transition ) let spacing: CGFloat = 6.0 let totalHeight: CGFloat = upgradeTitle.size.height + spacing + upgradeDescription.size.height - context.add(upgradeTitle - .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 - totalHeight / 2.0 + upgradeTitle.size.height / 2.0))) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - - context.add(upgradeDescription - .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 + totalHeight / 2.0 - upgradeDescription.size.height / 2.0))) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) + headerComponents.append({ + context.add(upgradeTitle + .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 - totalHeight / 2.0 + upgradeTitle.size.height / 2.0))) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + + context.add(upgradeDescription + .position(CGPoint(x: context.availableSize.width / 2.0, y: floor(212.0 + totalHeight / 2.0 - upgradeDescription.size.height / 2.0))) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) originY += 24.0 let textColor = theme.actionSheet.primaryTextColor @@ -2415,11 +2516,13 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: uniqueGift != nil ? 190.0 : 173.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) + headerComponents.append({ + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: uniqueGift != nil ? 190.0 : 173.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) var descriptionOffset: CGFloat = 0.0 if let subtitleString { @@ -2455,14 +2558,16 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - context.add(subtitle - .position(CGPoint(x: context.availableSize.width / 2.0, y: uniqueGift != nil ? 210.0 : 196.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) + headerComponents.append({ + context.add(subtitle + .position(CGPoint(x: context.availableSize.width / 2.0, y: uniqueGift != nil ? 210.0 : 196.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) descriptionOffset += subtitle.size.height } - + var useDescriptionTint = false if !descriptionText.isEmpty { var linkColor = theme.actionSheet.controlAccentColor if hasDescriptionButton { @@ -2478,6 +2583,7 @@ private final class GiftViewSheetContent: CombinedComponent { let textFont: UIFont let textColor: UIColor + if let _ = uniqueGift { textFont = Font.regular(13.0) if hasDescriptionButton { @@ -2485,11 +2591,12 @@ private final class GiftViewSheetContent: CombinedComponent { } else { textColor = vibrantColor } + useDescriptionTint = true } else { textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor } - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: useDescriptionTint ? .white : textColor), bold: MarkdownAttributeSet(font: textFont, textColor: useDescriptionTint ? .white : textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -2504,59 +2611,114 @@ private final class GiftViewSheetContent: CombinedComponent { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) } - let description = description.update( - component: MultilineTextComponent( - text: .plain(attributedString), - horizontalAlignment: .center, - maximumNumberOfLines: 5, - lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if !hasDescriptionButton, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { [weak state] attributes, _ in - if !hasDescriptionButton, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - state?.openStarsIntro() - } + var descriptionSize = CGSize() + if state.justUpgraded { + var items: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Collectible #", font: textFont, color: .white, tintColor: textColor))) + ] + + let spinningItems: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: "0", component: AnyComponent(Text(text: "0", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "1", component: AnyComponent(Text(text: "1", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "2", component: AnyComponent(Text(text: "2", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "3", component: AnyComponent(Text(text: "3", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "4", component: AnyComponent(Text(text: "4", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "5", component: AnyComponent(Text(text: "5", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "6", component: AnyComponent(Text(text: "6", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "7", component: AnyComponent(Text(text: "7", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "8", component: AnyComponent(Text(text: "8", font: textFont, color: textColor))), + AnyComponentWithIdentity(id: "9", component: AnyComponent(Text(text: "9", font: textFont, color: textColor))) + ] + if let numberValue = uniqueGift?.number { + let numberString = "\(numberValue)" + var i = 0 + for c in numberString { + items.append(AnyComponentWithIdentity(id: "c\(i)", component: AnyComponent(SlotsComponent( + item: AnyComponent(Text(text: String(c), font: textFont, color: .white)), + items: spinningItems, + isAnimating: i > state.revealedNumberDigits, + tintColor: textColor, + verticalOffset: -1.0 - UIScreenPixel, + motionBlur: false, + size: CGSize(width: 8.0, height: 14.0)))) + ) + i += 1 } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - context.add(description - .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + description.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - - if hasDescriptionButton { - let descriptionButton = descriptionButton.update( - component: PlainButtonComponent( - content: AnyComponent( - RoundedRectangle(color: UIColor.white.withAlphaComponent(0.15), cornerRadius: 9.5) - ), - effectAlignment: .center, - action: { [weak state] in - if let releasedByPeer { - state?.openPeer(releasedByPeer) + } + let animatedDescription = animatedDescription.update( + component: HStack(items, spacing: 0.0), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), + transition: context.transition + ) + descriptionSize = animatedDescription.size + headerComponents.append({ + context.add(animatedDescription + .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + animatedDescription.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) + } else { + let description = description.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + tintColor: useDescriptionTint ? textColor : nil, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if !hasDescriptionButton, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil } }, - animateScale: false + tapAction: { [weak state] attributes, _ in + if !hasDescriptionButton, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + state?.openStarsIntro() + } + } ), - environment: {}, - availableSize: CGSize(width: description.size.width + 18.0, height: 19.0), - transition: .immediate - ) - context.add(descriptionButton - .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + description.size.height / 2.0 - 1.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), + transition: context.transition ) + descriptionSize = description.size + headerComponents.append({ + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + description.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) + + if hasDescriptionButton { + let descriptionButton = descriptionButton.update( + component: PlainButtonComponent( + content: AnyComponent( + RoundedRectangle(color: UIColor.white.withAlphaComponent(0.15), cornerRadius: 9.5) + ), + effectAlignment: .center, + action: { [weak state] in + if let releasedByPeer { + state?.openPeer(releasedByPeer) + } + }, + animateScale: false + ), + environment: {}, + availableSize: CGSize(width: description.size.width + 18.0, height: 19.0), + transition: .immediate + ) + headerComponents.append({ + context.add(descriptionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + description.size.height / 2.0 - 1.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + }) + } } originY += descriptionOffset @@ -2564,7 +2726,7 @@ private final class GiftViewSheetContent: CombinedComponent { if uniqueGift != nil { originY += 16.0 } else { - originY += description.size.height + 21.0 + originY += descriptionSize.height + 21.0 if soldOut { originY -= 7.0 } @@ -2581,7 +2743,11 @@ private final class GiftViewSheetContent: CombinedComponent { if incoming { hiddenDescription = text != nil ? strings.Gift_View_NameAndMessageHidden : strings.Gift_View_NameHidden } else if let peerId = subject.arguments?.peerId, let peer = state.peerMap[peerId], subject.arguments?.fromPeerId != nil { - hiddenDescription = text != nil ? strings.Gift_View_Outgoing_NameAndMessageHidden(peer.compactDisplayTitle).string : strings.Gift_View_Outgoing_NameHidden(peer.compactDisplayTitle).string + var peerName = peer.compactDisplayTitle + if peerName.count > 30 { + peerName = "\(peerName.prefix(30))…" + } + hiddenDescription = text != nil ? strings.Gift_View_Outgoing_NameAndMessageHidden(peerName).string : strings.Gift_View_Outgoing_NameHidden(peerName).string } else { hiddenDescription = "" } @@ -2876,11 +3042,14 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: buttonWidth, height: buttonHeight), transition: context.transition ) - context.add(transferButton - .position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) + let buttonPosition = buttonOriginX + buttonWidth / 2.0 + headerComponents.append({ + context.add(transferButton + .position(CGPoint(x: buttonPosition, y: headerHeight - buttonHeight / 2.0 - 16.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + }) buttonOriginX += buttonWidth + buttonSpacing } @@ -2936,11 +3105,14 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: buttonWidth, height: buttonHeight), transition: context.transition ) - context.add(wearButton - .position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) + let buttonPosition = buttonOriginX + buttonWidth / 2.0 + headerComponents.append({ + context.add(wearButton + .position(CGPoint(x: buttonPosition, y: headerHeight - buttonHeight / 2.0 - 16.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + }) buttonOriginX += buttonWidth + buttonSpacing if canResell { @@ -2961,16 +3133,19 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: buttonWidth, height: buttonHeight), transition: context.transition ) - context.add(resellButton - .position(CGPoint(x: buttonOriginX + buttonWidth / 2.0, y: headerHeight - buttonHeight / 2.0 - 16.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) + let buttonPosition = buttonOriginX + buttonWidth / 2.0 + headerComponents.append({ + context.add(resellButton + .position(CGPoint(x: buttonPosition, y: headerHeight - buttonHeight / 2.0 - 16.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + }) } } let order: [StarGift.UniqueGift.Attribute.AttributeType] = [ - .model, .backdrop, .pattern, .originalInfo + .model, .pattern, .backdrop, .originalInfo ] var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:] @@ -2981,13 +3156,15 @@ private final class GiftViewSheetContent: CombinedComponent { var hasOriginalInfo = false for type in order { if let attribute = attributeMap[type] { - let id: String + var id: String let title: String? let value: NSAttributedString let percentage: Float? let tag: AnyObject? var hasBackground = false + var otherValuesAndPercentages: [(value: String, percentage: Float)] = [] + switch attribute { case let .model(name, _, rarity): id = "model" @@ -2995,18 +3172,42 @@ private final class GiftViewSheetContent: CombinedComponent { value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) percentage = Float(rarity) * 0.1 tag = state.modelButtonTag + + if state.justUpgraded, let sampleAttributes = state.sampleGiftAttributes { + for sampleAttribute in sampleAttributes { + if case let .model(name, _, rarity) = sampleAttribute { + otherValuesAndPercentages.append((name, Float(rarity) * 0.1)) + } + } + } case let .backdrop(name, _, _, _, _, _, rarity): id = "backdrop" title = strings.Gift_Unique_Backdrop value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) percentage = Float(rarity) * 0.1 tag = state.backdropButtonTag + + if state.justUpgraded, let sampleAttributes = state.sampleGiftAttributes { + for sampleAttribute in sampleAttributes { + if case let .backdrop(name, _, _, _, _, _, rarity) = sampleAttribute { + otherValuesAndPercentages.append((name, Float(rarity) * 0.1)) + } + } + } case let .pattern(name, _, rarity): id = "pattern" title = strings.Gift_Unique_Symbol value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) percentage = Float(rarity) * 0.1 tag = state.symbolButtonTag + + if state.justUpgraded, let sampleAttributes = state.sampleGiftAttributes { + for sampleAttribute in sampleAttributes { + if case let .pattern(name, _, rarity) = sampleAttribute { + otherValuesAndPercentages.append((name, Float(rarity) * 0.1)) + } + } + } case let .originalInfo(senderPeerId, recipientPeerId, date, text, entities): id = "originalInfo" title = nil @@ -3058,6 +3259,10 @@ private final class GiftViewSheetContent: CombinedComponent { hasOriginalInfo = true } + if !otherValuesAndPercentages.isEmpty { + id += "_reel" + } + var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( @@ -3108,9 +3313,41 @@ private final class GiftViewSheetContent: CombinedComponent { ).tagged(tag)) )) } - let itemComponent = AnyComponent( + var itemComponent = AnyComponent( HStack(items, spacing: 4.0) ) + + if !otherValuesAndPercentages.isEmpty { + var subitems: [AnyComponentWithIdentity] = [] + var index = 0 + + for (title, percentage) in otherValuesAndPercentages { + subitems.append( + AnyComponentWithIdentity(id: "anim_\(index)", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: title, font: tableFont, color: tableTextColor))), + AnyComponentWithIdentity(id: "rarity", component: AnyComponent(ButtonContentComponent( + context: component.context, + text: formatPercentage(percentage), + color: theme.list.itemAccentColor + ))) + ], spacing: 4.0) + )) + ) + index += 1 + } + + itemComponent = AnyComponent( + SlotsComponent( + item: itemComponent, + items: subitems, + isAnimating: !state.revealedAttributes.contains(type), + motionBlur: false, + size: CGSize(width: 160.0, height: 18.0) + ) + ) + } + tableItems.append(.init( id: id, title: title, @@ -3325,10 +3562,19 @@ private final class GiftViewSheetContent: CombinedComponent { context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) + .disappear(ComponentTransition.Disappear({ view, transition, completion in + view.superview?.insertSubview(view, at: 0) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + })) ) originY += table.size.height + 23.0 } + + for component in headerComponents { + component() + } var resellAmount: CurrencyAmount? var selling = false @@ -3994,7 +4240,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, _): var reference: StarGiftReference if let peerId, let giftMessageId { reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift new file mode 100644 index 0000000000..3908766bb5 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift @@ -0,0 +1,498 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ImageBlur + +private let additionalInset: CGFloat = 4.0 +private let maskInset: CGFloat = 8.0 + +final class SlotsComponent: Component { + public typealias EnvironmentType = ChildEnvironment + + private let item: AnyComponent + private let items: [AnyComponentWithIdentity] + private let isAnimating: Bool + private let tintColor: UIColor? + private let verticalOffset: CGFloat + private let motionBlur: Bool + private let size: CGSize + + public init( + item: AnyComponent, + items: [AnyComponentWithIdentity], + isAnimating: Bool, + tintColor: UIColor? = nil, + verticalOffset: CGFloat = 0.0, + motionBlur: Bool = true, + size: CGSize + ) { + self.item = item + self.items = items + self.isAnimating = isAnimating + self.tintColor = tintColor + self.verticalOffset = verticalOffset + self.motionBlur = motionBlur + self.size = size + } + + public static func == (lhs: SlotsComponent, rhs: SlotsComponent) -> Bool { + if lhs.item != rhs.item { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.isAnimating != rhs.isAnimating { + return false + } + if lhs.tintColor != rhs.tintColor { + return false + } + if lhs.verticalOffset != rhs.verticalOffset { + return false + } + if lhs.motionBlur != rhs.motionBlur { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + public final class View: UIView { + private var itemViews: [AnyHashable: ComponentView] = [:] + private var motionBlurLayers: [AnyHashable: SimpleLayer] = [:] + private var order: [AnyHashable] = [] + + private let containerView = UIView() + private let maskLayer = SimpleGradientLayer() + + private enum SpinState { + case idle + case spinning + case decelerating + case settled + } + + private var spinState: SpinState = .idle + private var isAnimating = false + private var animationLink: SharedDisplayLinkDriver.Link? + + private var currentIds = Set() + private var lastSpawnTime: Double? + private var currentInterval: Double = 0.09 + + private var motionBlurFactor: CGFloat = 1.0 + private var decelQueue: [AnyComponentWithIdentity] = [] + private var decelTotalSteps: Int = 0 + private var decelStepIndex: Int = 0 + + private let minSpawnInterval: Double = 0.10 + private let maxSpawnInterval: Double = 0.80 + + private let baseAnimDuration: Double = 0.18 + private let maxAnimDuration: Double = 0.5 + + private var component: SlotsComponent? + private var environment: Environment? + private var availableSize: CGSize? + + @inline(__always) private func lerp(_ a: Double, _ b: Double, _ t: Double) -> Double { a + (b - a) * t } + @inline(__always) private func clamp01(_ x: Double) -> Double { max(0.0, min(1.0, x)) } + + override init(frame: CGRect) { + super.init(frame: frame) + self.addSubview(self.containerView) + + self.containerView.clipsToBounds = true + + self.maskLayer.startPoint = CGPoint(x: 0.5, y: 0.0) + self.maskLayer.endPoint = CGPoint(x: 0.5, y: 1.0) + + let fade: CGFloat = 0.2 + self.maskLayer.locations = [0.0, NSNumber(value: Float(fade)), NSNumber(value: Float(1.0 - fade)), 1.0] + self.maskLayer.colors = [ + UIColor.black.withAlphaComponent(0.0).cgColor, + UIColor.black.withAlphaComponent(1.0).cgColor, + UIColor.black.withAlphaComponent(1.0).cgColor, + UIColor.black.withAlphaComponent(0.0).cgColor + ] + self.containerView.layer.mask = self.maskLayer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func spawnRandomSlot(availableSize: CGSize) { + guard var items = self.component?.items, !items.isEmpty else { return } + items = items.filter { !self.currentIds.contains($0.id) } + guard let randomItem = items.randomElement() else { return } + + self.spawnSlot(item: randomItem, availableSize: availableSize) {} + } + + private func spawnSlot( + item: AnyComponentWithIdentity, + isFinal: Bool = false, + availableSize: CGSize, + animDuration: Double? = nil, + completion: @escaping () -> Void + ) { + guard let component = self.component, let environment = self.environment else { return } + + self.currentIds.insert(item.id) + self.lastSpawnTime = CACurrentMediaTime() + + let itemView = self.itemViews[item.id] ?? ComponentView() + self.itemViews[item.id] = itemView + + let size = itemView.update( + transition: .immediate, + component: item.component, + environment: { environment[ChildEnvironment.self] }, + containerSize: availableSize + ) + if let view = itemView.view { + if view.superview == nil { + if let tintColor = component.tintColor { + view.layer.layerTintColor = tintColor.cgColor + } + view.layer.allowsGroupOpacity = true + self.containerView.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 0.0, y: -size.height - additionalInset), size: size) + + let travelDistance = (size.height + maskInset + additionalInset) * 2.0 + let pitch = (size.height + additionalInset) + + if isFinal { + var finalFrame = view.frame + finalFrame.origin.y = maskInset + view.frame = finalFrame + + let fromY = size.height + maskInset + additionalInset + let overshoot: CGFloat = 7.0 + + let anim = CAKeyframeAnimation(keyPath: "position.y") + anim.isAdditive = true + anim.values = [ fromY, -overshoot, 0.0 ] + anim.keyTimes = [0.0, 0.7, 1.0] + anim.timingFunctions = [ + CAMediaTimingFunction(name: .easeOut), + CAMediaTimingFunction(name: .easeInEaseOut) + ] + anim.duration = 0.5 + + CATransaction.begin() + CATransaction.setCompletionBlock { + completion() + self.currentIds.remove(item.id) + self.finishSettled() + } + view.layer.add(anim, forKey: "finalOvershoot") + CATransaction.commit() + } + else { + let duration: Double = animDuration ?? baseAnimDuration + + view.layer.animatePosition( + from: CGPoint(x: 0.0, y: (size.height + maskInset + additionalInset) * 2.0), + to: .zero, + duration: duration, + timingFunction: CAMediaTimingFunctionName.linear.rawValue, + additive: true, + completion: { _ in + completion() + self.currentIds.remove(item.id) + } + ) + + let intervalForConstantSpacing = Double(pitch / travelDistance) * duration + self.currentInterval = intervalForConstantSpacing + } + } + + self.setMotionBlurFactor(id: item.id, factor: self.motionBlurFactor, transition: .immediate) + } + + private func setMotionBlurFactor(id: AnyHashable, factor: CGFloat, transition: ComponentTransition) { + guard let component = self.component, component.motionBlur, let itemView = self.itemViews[id]?.view else { + return + } + + if factor != 0.0 { + let motionBlurLayer: SimpleLayer + if let current = self.motionBlurLayers[id] { + motionBlurLayer = current + } else { + motionBlurLayer = SimpleLayer() + + let image = generateImage(itemView.bounds.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { UIGraphicsPopContext() } + context.clear(CGRect(origin: CGPoint(), size: size)) + itemView.layer.render(in: context) + }) + if let image { + motionBlurLayer.contents = verticalBlurredImage(image, radius: 8.0)?.cgImage + } + motionBlurLayer.contentsScale = itemView.layer.contentsScale + self.motionBlurLayers[id] = motionBlurLayer + itemView.layer.addSublayer(motionBlurLayer) + + motionBlurLayer.position = CGPoint(x: itemView.bounds.size.width * 0.5, y: itemView.bounds.size.height * 0.5) + motionBlurLayer.bounds = CGRect(origin: CGPoint(), size: itemView.bounds.size) + + if let tintColor = component.tintColor { + motionBlurLayer.layerTintColor = tintColor.cgColor + } + } + + let scaleFactor = 1.0 * (1.0 - factor) + 1.25 * factor + let opacityFactor = 1.0 * (1.0 - factor) + 0.6 * factor + transition.setTransform(layer: motionBlurLayer, transform: CATransform3DMakeScale(1.0, scaleFactor, 1.0)) + transition.setAlpha(layer: itemView.layer, alpha: opacityFactor) + } else if let motionBlurLayer = self.motionBlurLayers[id] { + self.motionBlurLayers.removeValue(forKey: id) + transition.setAlpha(layer: motionBlurLayer, alpha: 0.0, completion: { [weak motionBlurLayer] _ in + motionBlurLayer?.removeFromSuperlayer() + }) + transition.setTransform(layer: motionBlurLayer, transform: CATransform3DIdentity) + } + } + + private func beginSpinning() { + self.spinState = .spinning + self.isAnimating = true + self.motionBlurFactor = 1.0 + self.currentInterval = 0.1 + + self.ensureDisplayLink() + } + + private func beginDeceleration() { + guard let component = self.component, self.spinState == .spinning || self.spinState == .decelerating else { return } + + self.spinState = .decelerating + self.isAnimating = false + + var queue: [AnyComponentWithIdentity] = [] + if !component.items.isEmpty { + let shuffled = Array(component.items.shuffled().prefix(3)) + queue.append(contentsOf: shuffled) + } + queue.append(AnyComponentWithIdentity(id: "final", component: component.item)) + + self.decelQueue = queue + self.decelTotalSteps = queue.count + self.decelStepIndex = 0 + + self.motionBlurFactor = max(self.motionBlurFactor, 0.0001) + + self.ensureDisplayLink() + } + + private func ensureDisplayLink() { + guard self.animationLink == nil else { + return + } + self.animationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + self?.tick() + }) + } + + private func invalidateDisplayLinkIfIdle() { + if self.spinState == .idle || self.spinState == .settled { + self.animationLink?.invalidate() + self.animationLink = nil + } + } + + private func applyEdge3DSquish() { + let H = self.containerView.bounds.height + guard H > 0 else { return } + + CATransaction.begin() + CATransaction.setDisableActions(true) + for (_, cv) in self.itemViews { + guard let v = cv.view, v.superview === self.containerView else { continue } + let midY = liveMidY(in: self.containerView, of: v) + let scaleY = edgeScaleY(for: midY, containerHeight: H) + var t = CATransform3DIdentity + t = CATransform3DScale(t, 1.0, scaleY, 1.0) + v.layer.transform = t + } + CATransaction.commit() + } + + private func tick() { + guard let availableSize = self.availableSize else { + return + } + let now = CACurrentMediaTime() + + switch self.spinState { + case .spinning: + if let last = self.lastSpawnTime, now - last >= self.currentInterval || self.lastSpawnTime == nil { + self.spawnRandomSlot(availableSize: availableSize) + } + case .decelerating: + let t = clamp01(self.decelTotalSteps > 1 + ? Double(self.decelStepIndex) / Double(self.decelTotalSteps - 1) + : 1.0) + + if let last = self.lastSpawnTime, now - last >= self.currentInterval { + if !self.decelQueue.isEmpty { + let next = self.decelQueue.removeFirst() + let isFinal = self.decelQueue.isEmpty + let animDuration = isFinal ? nil : lerp(baseAnimDuration, maxAnimDuration, t) + self.spawnSlot(item: next, isFinal: isFinal, availableSize: availableSize, animDuration: animDuration, completion: {}) + + self.motionBlurFactor = CGFloat(1.0 - t) + + self.decelStepIndex += 1 + } + } else if self.lastSpawnTime == nil { + if !self.decelQueue.isEmpty { + let next = self.decelQueue.removeFirst() + self.spawnSlot(item: next, availableSize: availableSize) {} + } + } + + case .settled, .idle: + self.invalidateDisplayLinkIfIdle() + } + + self.applyEdge3DSquish() + } + + private func finishSettled() { + for (id, _) in self.motionBlurLayers { + self.setMotionBlurFactor(id: id, factor: 0.0, transition: .easeInOut(duration: 0.2)) + } + self.motionBlurLayers.removeAll() + + self.spinState = .settled + self.decelQueue.removeAll() + self.invalidateDisplayLinkIfIdle() + } + + func update( + component: SlotsComponent, + availableSize: CGSize, + state: EmptyComponentState, + environment: Environment, + transition: ComponentTransition + ) -> CGSize { + self.component = component + self.environment = environment + self.availableSize = availableSize + + let size = component.size + self.containerView.frame = CGRect(origin: CGPoint(x: 0.0, y: component.verticalOffset), size: size).insetBy(dx: 0.0, dy: -maskInset) + self.maskLayer.frame = CGRect(origin: .zero, size: self.containerView.bounds.size) + + let wasAnimating = self.isAnimating + let nowAnimating = component.isAnimating + + if nowAnimating && !wasAnimating { + self.beginSpinning() + self.spawnRandomSlot(availableSize: availableSize) + } else if !nowAnimating && wasAnimating { + self.beginDeceleration() + } else if nowAnimating && self.spinState == .settled { + self.beginSpinning() + self.spawnRandomSlot(availableSize: availableSize) + } + + if let tintColor = component.tintColor { + for (id, itemView) in self.itemViews { + if let itemLayer = itemView.view?.layer { + transition.setTintColor(layer: itemLayer, color: tintColor) + } + if let blurLayer = self.motionBlurLayers[id] { + transition.setTintColor(layer: blurLayer, color: tintColor) + } + } + } + + return size + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private let minScaleYAtEdge: CGFloat = 0.7 +private let squishFalloff: CGFloat = 0.12 + +private func smoothstep(_ x: CGFloat) -> CGFloat { + let t = max(0.0, min(1.0, x)) + return t * t * (3.0 - 2.0 * t) +} + +private func liveMidY(in container: UIView, of view: UIView) -> CGFloat { + if let pres = view.layer.presentation() { + let p = container.layer.convert(pres.position, from: view.layer.superlayer) + return p.y + } + return view.center.y +} + +private func edgeScaleY(for midY: CGFloat, containerHeight H: CGFloat) -> CGFloat { + guard H > 0 else { return 1.0 } + let d = abs((midY - H * 0.5) / (H * 0.5)) + let uRaw = (d - squishFalloff) / (1.0 - squishFalloff) + let u = smoothstep(max(0.0, min(1.0, uRaw))) + return (1.0 - u) + minScaleYAtEdge * u +} + + +final class SpacerComponent: Component { + let size: CGSize + + init( + size: CGSize + ) { + self.size = size + } + + static func ==(lhs: SpacerComponent, rhs: SpacerComponent) -> Bool { + return lhs.size == rhs.size + } + + final class View: UIView { + private var component: SpacerComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SpacerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + return component.size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist/BUILD b/submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist/BUILD new file mode 100644 index 0000000000..25de730ac0 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerMessagesMediaPlaylist", + module_name = "PeerMessagesMediaPlaylist", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/MusicAlbumArtResources", + "//submodules/TextFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist/Sources/PeerMessagesMediaPlaylist.swift similarity index 92% rename from submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift rename to submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist/Sources/PeerMessagesMediaPlaylist.swift index e919761504..7cf71779a5 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist/Sources/PeerMessagesMediaPlaylist.swift @@ -47,22 +47,29 @@ private func extractFileMedia(_ message: Message) -> TelegramMediaFile? { return file } -final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { - let id: SharedMediaPlaylistItemId - let message: Message +public final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { + public let id: SharedMediaPlaylistItemId + public let message: Message + public let isSavedMusic: Bool - init(message: Message) { + public init(message: Message, isSavedMusic: Bool) { self.id = PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index) self.message = message + self.isSavedMusic = isSavedMusic } - var stableId: AnyHashable { + public var stableId: AnyHashable { return MessageMediaPlaylistItemStableId(stableId: message.stableId) } - lazy var playbackData: SharedMediaPlaybackData? = { + public lazy var playbackData: SharedMediaPlaybackData? = { if let file = extractFileMedia(self.message) { - let fileReference = FileMediaReference.message(message: MessageReference(self.message), media: file) + let fileReference: FileMediaReference + if self.isSavedMusic, let peer = self.message.peers[self.message.id.peerId], let peerReference = PeerReference(peer) { + fileReference = .savedMusic(peer: peerReference, media: file) + } else { + fileReference = .message(message: MessageReference(self.message), media: file) + } let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: self.message.isCopyProtected(), isViewOnce: self.message.minAutoremoveOrClearTimeout == viewOnceTimeout) for attribute in file.attributes { switch attribute { @@ -95,7 +102,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { return nil }() - lazy var displayData: SharedMediaPlaybackDisplayData? = { + public lazy var displayData: SharedMediaPlaybackDisplayData? = { if let file = extractFileMedia(self.message) { let text = self.message.text var entities: [MessageTextEntity] = [] @@ -385,16 +392,16 @@ private struct PlaybackStack { } } -final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { - let context: AccountContext +public final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { + public let context: AccountContext private let messagesLocation: PeerMessagesPlaylistLocation private let chatLocationContextHolder: Atomic? - var location: SharedMediaPlaylistLocation { + public var location: SharedMediaPlaylistLocation { return self.messagesLocation } - var currentItemDisappeared: (() -> Void)? + public var currentItemDisappeared: (() -> Void)? private let navigationDisposable = MetaDisposable() private let loadMoreDisposable = MetaDisposable() @@ -408,16 +415,16 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { private var loadingMore: Bool = false private var playedToEnd: Bool = false private var order: MusicPlaybackSettingsOrder = .regular - private(set) var looping: MusicPlaybackSettingsLooping = .none + public private(set) var looping: MusicPlaybackSettingsLooping = .none - let id: SharedMediaPlaylistId + public let id: SharedMediaPlaylistId private let stateValue = Promise() - var state: Signal { + public var state: Signal { return self.stateValue.get() } - init(context: AccountContext, location: PeerMessagesPlaylistLocation, chatLocationContextHolder: Atomic?) { + public init(context: AccountContext, location: PeerMessagesPlaylistLocation, chatLocationContextHolder: Atomic?) { assert(Queue.mainQueue().isCurrent()) self.id = location.playlistId @@ -426,13 +433,15 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.chatLocationContextHolder = chatLocationContextHolder self.messagesLocation = location - switch self.messagesLocation { + switch self.messagesLocation.effectiveLocation(context: context) { case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, _, messageId, _): self.loadItem(anchor: .messageId(messageId), navigation: .later, reversed: self.order == .reversed) case let .recentActions(message): self.loadingItem = false self.currentItem = (message, []) self.updateState() + case .savedMusic: + break } } @@ -442,7 +451,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.currentlyObservedMessageDisposable.dispose() } - func control(_ action: SharedMediaPlaylistControlAction) { + public func control(_ action: SharedMediaPlaylistControlAction) { assert(Queue.mainQueue().isCurrent()) switch action { @@ -488,7 +497,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } - func setOrder(_ order: MusicPlaybackSettingsOrder) { + public func setOrder(_ order: MusicPlaybackSettingsOrder) { if self.order != order { self.order = order self.playbackStack.clear() @@ -499,7 +508,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } - func setLooping(_ looping: MusicPlaybackSettingsLooping) { + public func setLooping(_ looping: MusicPlaybackSettingsLooping) { if self.looping != looping { self.looping = looping self.updateState() @@ -507,21 +516,26 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } private func updateState() { + var isSavedMusic = false + if case .savedMusic = self.messagesLocation { + isSavedMusic = true + } + var item: MessageMediaPlaylistItem? var nextItem: MessageMediaPlaylistItem? var previousItem: MessageMediaPlaylistItem? if let (message, aroundMessages) = self.currentItem { - item = MessageMediaPlaylistItem(message: message) + item = MessageMediaPlaylistItem(message: message, isSavedMusic: isSavedMusic) for around in aroundMessages { if around.index < message.index { - previousItem = MessageMediaPlaylistItem(message: around) + previousItem = MessageMediaPlaylistItem(message: around, isSavedMusic: isSavedMusic) } else { - nextItem = MessageMediaPlaylistItem(message: around) + nextItem = MessageMediaPlaylistItem(message: around, isSavedMusic: isSavedMusic) } } } self.stateValue.set(.single(SharedMediaPlaylistState(loading: self.loadingItem, playedToEnd: self.playedToEnd, item: item, nextItem: nextItem, previousItem: previousItem, order: self.order, looping: self.looping))) - if case .custom = self.messagesLocation { + if case .custom = self.messagesLocation.effectiveLocation(context: self.context) { } else if item?.message.id != self.currentlyObservedMessageId { self.currentlyObservedMessageId = item?.message.id if let id = item?.message.id { @@ -555,10 +569,10 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } else { namespaces = .not(Namespaces.Message.allNonRegular) } - + switch anchor { case let .messageId(messageId): - switch self.messagesLocation { + switch self.messagesLocation.effectiveLocation(context: self.context) { case let .messages(chatLocation, tagMask, _): let historySignal = self.context.account.postbox.messageAtId(messageId) |> take(1) @@ -598,25 +612,26 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.navigationDisposable.set((messages |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] messages in - if let strongSelf = self { - assert(strongSelf.loadingItem) - - strongSelf.loadingItem = false - if let message = messages.0.first(where: { $0.id == at }) { - strongSelf.playbackStack.clear() - strongSelf.playbackStack.push(message.id) - if let (message, aroundMessages, _) = navigatedMessageFromMessages(messages.0, anchorIndex: message.index, position: .exact) { - strongSelf.currentItem = (message, aroundMessages) - } else { - strongSelf.currentItem = (message, []) - } - strongSelf.playedToEnd = false - } else { - strongSelf.currentItem = nil - strongSelf.playedToEnd = true - } - strongSelf.updateState() + guard let self else { + return } + assert(self.loadingItem) + + self.loadingItem = false + if let message = messages.0.first(where: { $0.id == at }) { + self.playbackStack.clear() + self.playbackStack.push(message.id) + if let (message, aroundMessages, _) = navigatedMessageFromMessages(messages.0, anchorIndex: message.index, position: .exact) { + self.currentItem = (message, aroundMessages) + } else { + self.currentItem = (message, []) + } + self.playedToEnd = false + } else { + self.currentItem = nil + self.playedToEnd = true + } + self.updateState() })) default: self.navigationDisposable.set((self.context.account.postbox.messageAtId(messageId) @@ -638,7 +653,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { })) } case let .index(index): - switch self.messagesLocation { + switch self.messagesLocation.effectiveLocation(context: self.context) { case let .messages(chatLocation, tagMask, _): var inputIndex: Signal? let looping = self.looping @@ -770,6 +785,8 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { self.loadingItem = false self.currentItem = (message, []) self.updateState() + case .savedMusic: + fatalError() case let .custom(messages, _, _, loadMore): let inputIndex: Signal let looping = self.looping @@ -880,7 +897,7 @@ final class PeerMessagesMediaPlaylist: SharedMediaPlaylist { } } - func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { + public func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { if let item = item as? MessageMediaPlaylistItem { switch self.messagesLocation { case .recentActions: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 24b3b1e1ac..dccc9e198b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -278,8 +278,38 @@ public final class PeerInfoCoverComponent: Component { } } - public func animateTransition() { + public func animateSwipeTransition() { if let gradientSnapshotLayer = self.backgroundGradientLayer.snapshotContentTree() { + let backgroundSnapshotLayer = SimpleLayer() + backgroundSnapshotLayer.masksToBounds = true + backgroundSnapshotLayer.allowsGroupOpacity = true + backgroundSnapshotLayer.backgroundColor = self.backgroundView.backgroundColor?.cgColor + backgroundSnapshotLayer.frame = self.backgroundView.frame + self.layer.insertSublayer(backgroundSnapshotLayer, above: self.backgroundGradientLayer) + + gradientSnapshotLayer.frame = self.backgroundGradientLayer.convert(self.backgroundGradientLayer.bounds, to: self.backgroundView.layer) + backgroundSnapshotLayer.addSublayer(gradientSnapshotLayer) + + let mask = CAGradientLayer() + mask.startPoint = CGPoint(x: 0.0, y: 0.5) + mask.endPoint = CGPoint(x: 1.0, y: 0.5) + mask.frame = CGRect(origin: CGPoint(x: backgroundSnapshotLayer.bounds.width, y: 0.0), size: CGSize(width: backgroundSnapshotLayer.bounds.width * 2.0, height: backgroundSnapshotLayer.bounds.height)) + mask.colors = [ + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.cgColor, + UIColor.white.cgColor, + ] + mask.locations = [0.0, 0.5, 1.0] + backgroundSnapshotLayer.mask = mask + + mask.animatePosition(from: CGPoint(x: -backgroundSnapshotLayer.bounds.width * 2.0, y: 0.0), to: .zero, duration: 0.35, timingFunction: CAMediaTimingFunctionName.linear.rawValue, additive: true, completion: { [weak backgroundSnapshotLayer] _ in + backgroundSnapshotLayer?.removeFromSuperlayer() + }) + } + } + + public func animateTransition(background: Bool = true, bounce: Bool = true) { + if background, let gradientSnapshotLayer = self.backgroundGradientLayer.snapshotContentTree() { let backgroundSnapshotLayer = SimpleLayer() backgroundSnapshotLayer.allowsGroupOpacity = true backgroundSnapshotLayer.backgroundColor = self.backgroundView.backgroundColor?.cgColor @@ -301,8 +331,10 @@ public final class PeerInfoCoverComponent: Component { } layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - let values: [NSNumber] = [1.0, 1.08, 1.0] - self.avatarBackgroundPatternContentsLayer.animateKeyframes(values: values, duration: 0.25, keyPath: "sublayerTransform.scale") + if bounce { + let values: [NSNumber] = [1.0, 1.14, 1.0] + self.avatarBackgroundPatternContentsLayer.animateKeyframes(values: values, duration: 0.4, keyPath: "sublayerTransform.scale") + } } private func loadPatternFromFile() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 6096e4a327..9e56ec5e89 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -164,6 +164,7 @@ swift_library( "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/BottomButtonPanelComponent", "//submodules/TelegramUI/Components/MarqueeComponent", + "//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 52f8be80f5..d7a6ab2542 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -2634,6 +2634,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { return musicBackground }() musicTransition.updateFrame(view: musicBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - musicHeight - buttonRightOrigin.y), size: CGSize(width: backgroundFrame.width, height: musicHeight))) + + if let _ = self.navigationTransition { + transition.updateAlpha(layer: musicBackground.layer, alpha: 1.0 - transitionFraction) + } else { + musicTransition.updateAlpha(layer: musicBackground.layer, alpha: 1.0) + } } else if let musicBackground = self.musicBackground { self.musicBackground = nil if transition.isAnimated { @@ -2685,10 +2691,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { environment: {}, containerSize: CGSize(width: backgroundFrame.width, height: musicHeight) ) - let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - musicHeight), size: musicSize) + let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: (apparentBackgroundHeight - backgroundHeight) + backgroundHeight - musicHeight), size: musicSize) if let musicView = music.view { if musicView.superview == nil { - self.view.addSubview(musicView) + self.regularContentNode.view.addSubview(musicView) if transition.isAnimated { musicView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -2698,7 +2704,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { musicTransition.updateFrame(view: musicView, frame: musicFrame) } - musicTransition.updateAlpha(layer: musicView.layer, alpha: backgroundBannerAlpha) + + if let _ = self.navigationTransition { + transition.updateAlpha(layer: musicView.layer, alpha: 1.0 - transitionFraction) + } else { + musicTransition.updateAlpha(layer: musicView.layer, alpha: backgroundBannerAlpha) + } } } else { if let musicBackground = self.musicBackground { @@ -2825,6 +2836,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { return giftsCoverView } + if let musicView = self.music?.view, let result = musicView.hitTest(self.view.convert(point, to: musicView), with: event) { + return result + } + if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { return nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 15eb470c1e..4a7afc830c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -1639,7 +1639,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat environment: {}, containerSize: tabsContainerSize ) - let tabContainerFrameOriginX = floorToScreenPixels((size.width - tabsContainerEffectiveSize.width) / 2.0) + let tabContainerFrameOriginX = items.count == 1 ? sideInset : floorToScreenPixels((size.width - tabsContainerEffectiveSize.width) / 2.0) let tabContainerFrame = CGRect(origin: CGPoint(x: tabContainerFrameOriginX, y: 10.0 - tabsOffset), size: tabsContainerSize) if let tabsContainerView = self.tabsContainer.view { if tabsContainerView.superview == nil { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 68ecabb5b0..e1a4d9139a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -112,6 +112,7 @@ import OldChannelsController import UrlHandling import VerifyAlertController import GiftViewScreen +import PeerMessagesMediaPlaylist public enum PeerInfoAvatarEditingMode { case generic @@ -6013,61 +6014,56 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } let peerId = self.peerId - let peer = self.data?.peer - let initialMessageId: MessageId + //let peer = self.data?.peer + let initialId: Int32 if let initialFileId = self.data?.savedMusicState?.files.first?.fileId { - initialMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(clamping: initialFileId.id % Int64(Int32.max))) + initialId = Int32(clamping: initialFileId.id % Int64(Int32.max)) } else { - initialMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0) + initialId = 0 } + + let playlistLocation: PeerMessagesPlaylistLocation = .savedMusic(context: savedMusicContext, at: initialId, canReorder: peerId == self.context.account.peerId) - let musicController = self.context.sharedContext.makeOverlayAudioPlayerController( - context: self.context, - chatLocation: .peer(id: peerId), - type: .music, - initialMessageId: initialMessageId, - initialOrder: .regular, - playlistLocation: PeerMessagesPlaylistLocation.custom( - messages: savedMusicContext.state - |> map { state in - var messages: [Message] = [] - var peers = SimpleDictionary() - peers[peerId] = peer - for file in state.files { - let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max)) - messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])) - - } - return (messages, Int32(messages.count), true) - }, - canReorder: peerId == self.context.account.peerId, - at: initialMessageId, - loadMore: { [weak savedMusicContext] in + let _ = (self.context.sharedContext.mediaManager.globalMediaPlayerState + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] accountStateAndType in + guard let self else { + return + } + if let (account, stateOrLoading, _) = accountStateAndType, self.context.account.peerId == account.peerId, case let .state(state) = stateOrLoading, let location = state.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(savedMusicContext, _, _) = location, savedMusicContext.peerId == peerId { + } else { + self.context.sharedContext.mediaManager.setPlaylist((self.context, PeerMessagesMediaPlaylist(context: self.context, location: playlistLocation, chatLocationContextHolder: nil)), type: .music, control: .playback(.play)) + } + }) + + Queue.mainQueue().after(0.1) { + let musicController = self.context.sharedContext.makeOverlayAudioPlayerController( + context: self.context, + chatLocation: .peer(id: peerId), + type: .music, + initialMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: initialId), + initialOrder: .regular, + playlistLocation: playlistLocation, + parentNavigationController: self.controller?.navigationController as? NavigationController, + updateMusicSaved: { [weak savedMusicContext] file, isSaved in guard let savedMusicContext else { return } - savedMusicContext.loadMore() + if isSaved { + let _ = savedMusicContext.addMusic(file: file).start() + } else { + let _ = savedMusicContext.removeMusic(file: file).start() + } + }, + reorderSavedMusic: { [weak savedMusicContext] file, afterFile in + guard let savedMusicContext else { + return + } + let _ = savedMusicContext.addMusic(file: file, afterFile: afterFile, apply: true).start() } - ), - parentNavigationController: self.controller?.navigationController as? NavigationController, - updateMusicSaved: { [weak savedMusicContext] file, isSaved in - guard let savedMusicContext else { - return - } - if isSaved { - let _ = savedMusicContext.addMusic(file: file).start() - } else { - let _ = savedMusicContext.removeMusic(file: file).start() - } - }, - reorderSavedMusic: { [weak savedMusicContext] file, afterFile in - guard let savedMusicContext else { - return - } - let _ = savedMusicContext.addMusic(file: file, afterFile: afterFile, apply: true).start() - } - ) - self.controller?.present(musicController, in: .window(.root)) + ) + self.controller?.present(musicController, in: .window(.root)) + } } private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift index f65937faf1..3ec904d2a4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftContextPreviewController.swift @@ -69,7 +69,7 @@ private final class GiftContextPreviewComponent: Component { case let .generic(gift): subject = .generic(gift.file) case let .unique(gift): - subject = .unique(gift) + subject = .unique(nil, gift) } let animationSize = self.animation.update( @@ -82,7 +82,7 @@ private final class GiftContextPreviewComponent: Component { animationScale: nil, displayAnimationStars: false, externalState: self.giftCompositionExternalState, - requestUpdate: { [weak state] in + requestUpdate: { [weak state] _ in state?.updated() } )), diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 97e399a610..a727875cea 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -676,7 +676,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { case let .gift(peerId): purpose = .starsGift(peerId: peerId, count: product.count, currency: currency, amount: amount) default: - purpose = .stars(count: product.count, currency: currency, amount: amount) + purpose = .stars(count: product.count, currency: currency, amount: amount, peerId: nil) } let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 846115bada..9d36fd41a0 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -390,7 +390,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { giftAnimationSubject = .generic(gift.file) giftAvailability = gift.availability case let .unique(gift): - giftAnimationSubject = .unique(gift) + giftAnimationSubject = .unique(nil, gift) } isGiftUpgrade = transaction.flags.contains(.isStarGiftUpgrade) } else if let giveawayMessageIdValue = transaction.giveawayMessageId { diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/Contents.json new file mode 100644 index 0000000000..562f9b14b9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "TimeLocked.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/TimeLocked.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/TimeLocked.pdf new file mode 100644 index 0000000000..5a20922cb0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/TimeLocked.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index f9992b613e..25cdc53378 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -553,7 +553,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return (messages, Int32(messages.count), false) } - source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, updateAll: true, canReorder: false, loadMore: nil) + source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, isSavedMusic: false, canReorder: false, loadMore: nil) case let .reply(reply): let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId)) |> map { messages, accountPeer -> ([Message], Int32, Bool) in @@ -567,7 +567,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return (messages, Int32(messages.count), false) } - source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote.flatMap { quote in ChatHistoryListSource.Quote(text: quote.text, offset: quote.offset) }, updateAll: true, canReorder: false, loadMore: nil) + source = .custom(messages: messages, messageId: messageIds.first ?? MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: reply.quote.flatMap { quote in ChatHistoryListSource.Quote(text: quote.text, offset: quote.offset) }, isSavedMusic: false, canReorder: false, loadMore: nil) case let .link(link): let messages = link.options |> mapToSignal { options -> Signal<(ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]), NoError> in @@ -668,13 +668,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return ([message], 1, false) } - source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, updateAll: true, canReorder: false, loadMore: nil) + source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, isSavedMusic: false, canReorder: false, loadMore: nil) } } else if case .customChatContents = chatLocation { if case let .customChatContents(customChatContents) = subject { source = .customView(historyView: customChatContents.historyView) } else { - source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, updateAll: true, canReorder: false, loadMore: nil) + source = .custom(messages: .single(([], 0, false)), messageId: nil, quote: nil, isSavedMusic: false, canReorder: false, loadMore: nil) } } else { source = .default diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 38584f4964..ca9603591b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -216,7 +216,7 @@ extension ListMessageItemInteraction { } } -private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, canReorder: Bool, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { var disableFloatingDateHeaders = false if case .customChatContents = chatLocation { disableFloatingDateHeaders = true @@ -239,7 +239,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, canReorder: canReorder) + item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, isSavedMusic: isSavedMusic, canReorder: canReorder) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -275,7 +275,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, canReorder: Bool, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { var disableFloatingDateHeaders = false if case .customChatContents = chatLocation { disableFloatingDateHeaders = true @@ -298,7 +298,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca case .allButLast: displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId } - item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, canReorder: canReorder) + item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch, isSavedMusic: isSavedMusic, canReorder: canReorder) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case let .MessageGroupEntry(_, messages, presentationData): @@ -334,8 +334,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca } } -private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, canReorder: Bool, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, canReorder: canReorder, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, canReorder: canReorder, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter) +private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, isSavedMusic: Bool, canReorder: Bool, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter) } final class ChatHistoryTransactionOpaqueState { @@ -1388,10 +1388,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var historyViewUpdate: Signal<(ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange?, Set), NoError> var isFirstTime = true - var updateAllOnEachVersion = false + var isSavedMusic = false var canReorder = false - if case let .custom(messages, at, quote, updateAll, canReorderValue, _) = self.source { - updateAllOnEachVersion = updateAll + if case let .custom(messages, at, quote, isSavedMusicValue, canReorderValue, _) = self.source { + isSavedMusic = isSavedMusicValue canReorder = canReorderValue historyViewUpdate = messages |> map { messages, _, hasMore in @@ -1909,7 +1909,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let forceSynchronous = true let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: nil, scrollAnimationCurve: nil, initialData: initialData?.initialData, keyboardButtonsMessage: nil, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: false, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: false) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, isSavedMusic: isSavedMusic, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) @@ -2299,8 +2299,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto keyboardButtonsMessage = nil } - let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion || forceUpdateAll) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: !isSavedMusic || forceUpdateAll) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) diff --git a/submodules/TelegramUI/Sources/MediaManager.swift b/submodules/TelegramUI/Sources/MediaManager.swift index 1d7556ecec..a15b8172b7 100644 --- a/submodules/TelegramUI/Sources/MediaManager.swift +++ b/submodules/TelegramUI/Sources/MediaManager.swift @@ -15,6 +15,7 @@ import TelegramUniversalVideoContent import DeviceProximity import MediaResources import PhotoResources +import PeerMessagesMediaPlaylist enum SharedMediaPlayerGroup: Int { case music = 0 diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 6abd752090..8d1943adcc 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -25,6 +25,7 @@ import GalleryData import StoryContainerScreen import WallpaperGalleryScreen import BrowserUI +import PeerMessagesMediaPlaylist func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { var story: TelegramMediaStory? diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index fd9589a119..5c7be90eb8 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -70,77 +70,76 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer override public func loadDisplayNode() { self.displayNode = OverlayAudioPlayerControllerNode(context: self.context, chatLocation: self.chatLocation, type: self.type, initialMessageId: self.initialMessageId, initialOrder: self.initialOrder, playlistLocation: self.playlistLocation, requestDismiss: { [weak self] in self?.dismiss() - }, requestShare: { [weak self] messageId in + }, requestShare: { [weak self] subject in if let strongSelf = self { - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) - |> deliverOnMainQueue).startStandalone(next: { message in - if let strongSelf = self, let message = message { - let shareController = ShareController(context: strongSelf.context, subject: .messages([message._asMessage()]), showInChat: { message in - if let strongSelf = self { - strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: message.id.peerId, messageId: message.id) - strongSelf.dismiss() - } - }, externalShare: true) - shareController.completed = { [weak self] peerIds in - if let strongSelf = self { - let _ = (strongSelf.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in - if let strongSelf = self { - let peers = peerList.compactMap { $0 } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { - text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" - } - } - - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.parentNavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) - } - return false - }), in: .current) - } - }) - } - } - strongSelf.controllerNode.view.endEditing(true) - strongSelf.present(shareController, in: .window(.root)) + var canShowInChat = false + if case .messages = subject { + canShowInChat = true + } + let shareController = ShareController(context: strongSelf.context, subject: subject, showInChat: canShowInChat ? { message in + if let strongSelf = self { + strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: message.id.peerId, messageId: message.id) + strongSelf.dismiss() } - }) + } : nil, externalShare: true) + shareController.completed = { [weak self] peerIds in + if let strongSelf = self { + let _ = (strongSelf.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + if let strongSelf = self { + let peers = peerList.compactMap { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.parentNavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + } + }) + } + } + strongSelf.controllerNode.view.endEditing(true) + strongSelf.present(shareController, in: .window(.root)) } }, requestSearchByArtist: { [weak self] artist in if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index acefc1a9fe..337d5f7cef 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -25,7 +25,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu private var presentationData: PresentationData private let type: MediaManagerPlayerType private let requestDismiss: () -> Void - private let requestShare: (MessageId) -> Void + private let requestShare: (ShareControllerSubject) -> Void private let requestSearchByArtist: (String) -> Void private let updateMusicSaved: (FileMediaReference, Bool) -> Void private let playlistLocation: SharedMediaPlaylistLocation? @@ -66,7 +66,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, requestDismiss: @escaping () -> Void, - requestShare: @escaping (MessageId) -> Void, + requestShare: @escaping (ShareControllerSubject) -> Void, requestSearchByArtist: @escaping (String) -> Void, updateMusicSaved: @escaping (FileMediaReference, Bool) -> Void, getParentController: @escaping () -> ViewController? @@ -82,8 +82,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.updateMusicSaved = updateMusicSaved self.getParentController = getParentController - if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, canReorder, at, loadMore) = playlistLocation { - self.source = .custom(messages: messages, messageId: at, quote: nil, updateAll: false, canReorder: canReorder, loadMore: loadMore) + if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, canReorder, at, loadMore) = playlistLocation.effectiveLocation(context: context) { + self.source = .custom(messages: messages, messageId: at, quote: nil, isSavedMusic: true, canReorder: canReorder, loadMore: loadMore) self.isGlobalSearch = false } else { self.source = .default @@ -301,8 +301,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self?.requestDismiss() } - self.controlsNode.requestShare = { [weak self] messageId in - self?.requestShare(messageId) + self.controlsNode.requestShare = { [weak self] subject in + self?.requestShare(subject) } self.controlsNode.requestSearchByArtist = { [weak self] artist in @@ -363,8 +363,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu openMessageImpl = { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.historyNode.messageInCurrentHistoryView(id) { var playlistLocation: PeerMessagesPlaylistLocation? - if let location = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, canReorder, _, loadMore) = location { - playlistLocation = .custom(messages: messages, canReorder: canReorder, at: id, loadMore: loadMore) + if let location = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation { + if case let .custom(messages, canReorder, _, loadMore) = location { + playlistLocation = .custom(messages: messages, canReorder: canReorder, at: id, loadMore: loadMore) + } else if case let .savedMusic(context, _, canReorder) = location { + playlistLocation = .savedMusic(context: context, at: id.id, canReorder: canReorder) + } } return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, openConferenceCall: { _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: playlistLocation)) @@ -1101,7 +1105,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu // ) var canDelete = false - if message.id.namespace == Namespaces.Message.Local { + if case let .custom(_, _, _, _, canReorder, _) = self.source, canReorder { canDelete = true } else if let peer = message.peers[message.id.peerId] { if peer is TelegramUser || peer is TelegramSecretChat { diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index d18bc300b3..a25d701572 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -198,7 +198,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { var updateIsExpanded: (() -> Void)? var requestCollapse: (() -> Void)? - var requestShare: ((MessageId) -> Void)? + var requestShare: ((ShareControllerSubject) -> Void)? var requestSearchByArtist: ((String) -> Void)? var requestSaveToProfile: ((FileMediaReference) -> Void)? var requestRemoveFromProfile: ((FileMediaReference) -> Void)? @@ -1027,9 +1027,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode { if hasSectionHeader { //TODO:localize + let sideInset: CGFloat = 16.0 var sectionTitle = "AUDIO IN THIS CHAT" - if let peerName = self.peerName { - sectionTitle = "\(peerName)'S PLAYLIST" + if var peerName = self.peerName { + if peerName.count > 30 { + peerName = "\(peerName.prefix(30))…" + } + sectionTitle = "\(peerName.uppercased())'S PLAYLIST" } else if case .custom = self.source { sectionTitle = "YOUR PLAYLIST" } @@ -1039,14 +1043,14 @@ final class OverlayPlayerControlsNode: ASDisplayNode { MultilineTextComponent(text: .plain(NSAttributedString(string: sectionTitle, font: Font.regular(13.0), textColor: self.presentationData.theme.chatList.sectionHeaderTextColor))) ), environment: {}, - containerSize: CGSize(width: width, height: OverlayPlayerControlsNode.sectionHeaderHeight) + containerSize: CGSize(width: width - leftInset - rightInset - sideInset * 2.0, height: OverlayPlayerControlsNode.sectionHeaderHeight) ) if let sectionTitleView = self.sectionTitle.view { if sectionTitleView.superview == nil { self.view.addSubview(sectionTitleView) } sectionTitleView.bounds = CGRect(origin: .zero, size: sectionTitleSize) - sectionHeaderTransition.updateFrame(view: sectionTitleView, frame: CGRect(origin: CGPoint(x: leftInset + 16.0, y: finalPanelHeight - OverlayPlayerControlsNode.sectionHeaderHeight + 6.0 + UIScreenPixel), size: sectionTitleSize)) + sectionHeaderTransition.updateFrame(view: sectionTitleView, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: finalPanelHeight - OverlayPlayerControlsNode.sectionHeaderHeight + 6.0 + UIScreenPixel), size: sectionTitleSize)) } } else if let sectionTitleView = self.sectionTitle.view, sectionTitleView.superview != nil { sectionTitleView.removeFromSuperview() @@ -1184,7 +1188,17 @@ final class OverlayPlayerControlsNode: ASDisplayNode { @objc func sharePressed() { if let itemId = self.currentItemId as? PeerMessagesMediaPlaylistItemId { - self.requestShare?(itemId.messageId) + if itemId.messageId.namespace == Namespaces.Message.Cloud { + let _ = (self.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: itemId.messageId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] message in + guard let message else { + return + } + self?.requestShare?(.messages([message._asMessage()])) + }) + } else if let fileReference = self.currentFileReference { + self.requestShare?(.media(fileReference.abstract, nil)) + } } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3dc718dbdf..21ab281ae6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1787,9 +1787,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?, updateMusicSaved: ((FileMediaReference, Bool) -> Void)?, reorderSavedMusic: ((FileMediaReference, FileMediaReference?) -> Void)?) -> ViewController & OverlayAudioPlayerController { - if let playlistLocation = playlistLocation as? PeerMessagesPlaylistLocation, case .custom = playlistLocation { - context.sharedContext.mediaManager.setPlaylist((context, PeerMessagesMediaPlaylist(context: context, location: playlistLocation, chatLocationContextHolder: nil)), type: .music, control: .playback(.play)) - } return OverlayAudioPlayerControllerImpl(context: context, chatLocation: chatLocation, type: type, initialMessageId: initialMessageId, initialOrder: initialOrder, playlistLocation: playlistLocation, parentNavigationController: parentNavigationController, updateMusicSaved: updateMusicSaved, reorderSavedMusic: reorderSavedMusic) } diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 709dfaeab8..7d48f091fa 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -9,6 +9,7 @@ import TelegramAudio import AccountContext import TelegramUniversalVideoContent import DeviceProximity +import PeerMessagesMediaPlaylist private enum SharedMediaPlaybackItem: Equatable { case audio(MediaPlayer) diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index b2537ee679..55f1cc2c25 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -80,7 +80,7 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M private let syntaxHighlighter = Syntaxer() -public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, codeBlockTitleColor: UIColor? = nil, codeBlockAccentColor: UIColor? = nil, codeBlockBackgroundColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil) -> NSAttributedString { +public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, codeBlockTitleColor: UIColor? = nil, codeBlockAccentColor: UIColor? = nil, codeBlockBackgroundColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, paragraphAlignment: NSTextAlignment? = nil) -> NSAttributedString { let baseQuoteTintColor = baseQuoteTintColor ?? baseColor var nsString: NSString? @@ -356,6 +356,12 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } }) + if let paragraphAlignment = paragraphAlignment { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = paragraphAlignment + string.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length)) + } + return string }