Various improvements

This commit is contained in:
Ilya Laktyushin 2025-08-22 21:46:07 +04:00
parent 75c773e694
commit a60a437206
56 changed files with 2051 additions and 485 deletions

View file

@ -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";

View file

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

View file

@ -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<PeerId, Peer>()
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))

View file

@ -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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize)
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View file

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

View file

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

View file

@ -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<MediaPlayerStatus?, NoError> {
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> Signal<MediaPlayerStatus?, NoError> {
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<MediaPlayerStatus?, NoError> 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<MediaPlayerStatus, NoError> {
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> Signal<MediaPlayerStatus, NoError> {
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<Float, NoError> {
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> Signal<Float, NoError> {
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<FileMediaResourceStatus, NoError> {
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<FileMediaResourceStatus, NoError> {
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList, isSavedMusic: isSavedMusic) |> map { status -> MediaPlayerPlaybackStatus? in
return status?.status
}

View file

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

View file

@ -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<FileMediaResourceStatus, NoError> 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)

View file

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

View file

@ -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<ChatLocationContextHolder?>(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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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<PendingMessageUploadedContentResult, PendingMessageUploadError>? {
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<PendingMessageUploadedContentResult, PendingMessageUploadError>? {
if let paidContent = media as? TelegramMediaPaidContent {
var signals: [Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>] = []
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))
}
}

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

View file

@ -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<StarGift.UniqueGift.Attribute.AttributeType>
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<StarGift.UniqueGift.Attribute.AttributeType> = 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; well 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<Empty>, 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)
}

View file

@ -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<Empty>()
@ -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

View file

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

View file

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

View file

@ -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(

View file

@ -53,6 +53,7 @@ swift_library(
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/ImageBlur",
],
visibility = [
"//visibility:public",

View file

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

View file

@ -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<StarGift.UniqueGift.Attribute.AttributeType>()
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<Empty>.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<Empty>] = [
AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Collectible #", font: textFont, color: .white, tintColor: textColor)))
]
let spinningItems: [AnyComponentWithIdentity<Empty>] = [
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<Empty>] = []
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<Empty>] = []
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))

View file

@ -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<ChildEnvironment: Equatable>: Component {
public typealias EnvironmentType = ChildEnvironment
private let item: AnyComponent<ChildEnvironment>
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let isAnimating: Bool
private let tintColor: UIColor?
private let verticalOffset: CGFloat
private let motionBlur: Bool
private let size: CGSize
public init(
item: AnyComponent<ChildEnvironment>,
items: [AnyComponentWithIdentity<ChildEnvironment>],
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<ChildEnvironment>, rhs: SlotsComponent<ChildEnvironment>) -> 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<ChildEnvironment>] = [:]
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<AnyHashable>()
private var lastSpawnTime: Double?
private var currentInterval: Double = 0.09
private var motionBlurFactor: CGFloat = 1.0
private var decelQueue: [AnyComponentWithIdentity<ChildEnvironment>] = []
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<ChildEnvironment>?
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<ChildEnvironment>,
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<ChildEnvironment>()
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<ChildEnvironment>] = []
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<ChildEnvironment>,
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<EnvironmentType>, 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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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",
],
)

View file

@ -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<ChatLocationContextHolder?>?
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<SharedMediaPlaylistState>()
var state: Signal<SharedMediaPlaylistState, NoError> {
public var state: Signal<SharedMediaPlaylistState, NoError> {
return self.stateValue.get()
}
init(context: AccountContext, location: PeerMessagesPlaylistLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?) {
public init(context: AccountContext, location: PeerMessagesPlaylistLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?) {
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<MessageIndex?, NoError>?
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<MessageIndex, NoError>
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:

View file

@ -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() {

View file

@ -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",

View file

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

View file

@ -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 {

View file

@ -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<PeerId, Peer>()
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?) {

View file

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

View file

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

View file

@ -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 {

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "TimeLocked.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

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

View file

@ -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<Int32>?, Set<MessageId>), 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)

View file

@ -15,6 +15,7 @@ import TelegramUniversalVideoContent
import DeviceProximity
import MediaResources
import PhotoResources
import PeerMessagesMediaPlaylist
enum SharedMediaPlayerGroup: Int {
case music = 0

View file

@ -25,6 +25,7 @@ import GalleryData
import StoryContainerScreen
import WallpaperGalleryScreen
import BrowserUI
import PeerMessagesMediaPlaylist
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
var story: TelegramMediaStory?

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import TelegramAudio
import AccountContext
import TelegramUniversalVideoContent
import DeviceProximity
import PeerMessagesMediaPlaylist
private enum SharedMediaPlaybackItem: Equatable {
case audio(MediaPlayer)

View file

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