mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Various improvements
This commit is contained in:
parent
75c773e694
commit
a60a437206
56 changed files with 2051 additions and 485 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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; we’ll animate X only
|
||||
let containerY = geom.centerY - containerHeight / 2.0
|
||||
container.frame.origin.y = containerY
|
||||
self.addSubview(container)
|
||||
self.decelContainer = container
|
||||
self.decelItemHosts.removeAll()
|
||||
|
||||
// Fill container with hosts at fixed pitch
|
||||
for (i, attribute) in tail.reversed().enumerated() {
|
||||
guard case let .model(_, file, _) = attribute else { continue }
|
||||
|
||||
// Node
|
||||
let node = DefaultAnimatedStickerNodeImpl()
|
||||
node.isUserInteractionEnabled = false
|
||||
let pathPrefix = self.component!.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||
node.setup(
|
||||
source: AnimatedStickerResourceSource(account: self.component!.context.account, resource: file.resource, isVideo: file.isVideoSticker),
|
||||
width: Int(iconSize.width * 1.6),
|
||||
height: Int(iconSize.height * 1.6),
|
||||
playbackMode: .still(.start),
|
||||
mode: .direct(cachePathPrefix: pathPrefix)
|
||||
)
|
||||
node.updateLayout(size: iconSize)
|
||||
node.visibility = true
|
||||
if i < 4 {
|
||||
node.playOnce();
|
||||
}
|
||||
|
||||
let host = node.view
|
||||
host.bounds = CGRect(origin: .zero, size: iconSize)
|
||||
host.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
|
||||
let hostView = UIView(frame: CGRect(origin: CGPoint(x: CGFloat(i) * pitch, y: 0), size: visualSize))
|
||||
host.center = CGPoint(x: visualSize.width / 2.0, y: visualSize.height / 2.0)
|
||||
hostView.addSubview(host)
|
||||
container.addSubview(hostView)
|
||||
self.decelItemHosts.append(hostView)
|
||||
|
||||
if i == 0 {
|
||||
self.animationNode = node
|
||||
|
||||
let factors: [CGFloat] = [1.0, 1.3, 0.92, 1.18, 0.98, 1.0]
|
||||
let values = factors.map { NSNumber(value: Double($0)) }
|
||||
let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale")
|
||||
scaleAnim.beginTime = CACurrentMediaTime() + 0.6
|
||||
scaleAnim.values = values
|
||||
scaleAnim.keyTimes = [0.0, 0.35, 0.55, 0.75, 0.9, 1.0].map(NSNumber.init)
|
||||
scaleAnim.timingFunctions = [
|
||||
CAMediaTimingFunction(name: .easeOut),
|
||||
CAMediaTimingFunction(name: .easeIn),
|
||||
CAMediaTimingFunction(name: .easeOut),
|
||||
CAMediaTimingFunction(name: .easeIn),
|
||||
CAMediaTimingFunction(name: .easeOut)
|
||||
]
|
||||
scaleAnim.duration = 0.85
|
||||
scaleAnim.isRemovedOnCompletion = true
|
||||
host.layer.add(scaleAnim, forKey: "bounce")
|
||||
}
|
||||
|
||||
if i == 1 {
|
||||
hostView.alpha = 0.0
|
||||
hostView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.7)
|
||||
}
|
||||
}
|
||||
|
||||
container.frame.origin.x = floor((availableSize.width - visualSize.width) / 2.0)
|
||||
|
||||
container.layer.animatePosition(
|
||||
from: CGPoint(x: -containerWidth - visualSize.width * 0.5 + containerWidth / 2.0 - self.spacingX - 70.0, y: container.frame.center.y),
|
||||
to: CGPoint(x: container.frame.center.x, y: container.frame.center.y),
|
||||
duration: self.maxAnimDuration,
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
completion: { [weak self] _ in
|
||||
guard let self, let container = self.decelContainer else {
|
||||
return
|
||||
}
|
||||
let _ = container
|
||||
//self.handleDecelArrived(container: container, iconSize: iconSize, visualSize: visualSize)
|
||||
}
|
||||
)
|
||||
|
||||
self.spinState = .decelerating
|
||||
self.spinLink?.invalidate()
|
||||
self.spinLink = nil
|
||||
}
|
||||
|
||||
private func handleDecelArrived(container: UIView, iconSize: CGSize, visualSize: CGSize) {
|
||||
let isFinalIndex = self.decelItemHosts.count - 1
|
||||
guard isFinalIndex >= 0, let finalHostView = self.decelItemHosts.last else { return }
|
||||
|
||||
// Prepare final node
|
||||
guard let node = self.animationNode as? DefaultAnimatedStickerNodeImpl else { return }
|
||||
node.playbackMode = .once
|
||||
node.visibility = true
|
||||
|
||||
// Convert final host center into self coords
|
||||
let finalCenterInSelf = container.convert(finalHostView.center, to: self)
|
||||
|
||||
// Place node directly in self, exactly where the final host is
|
||||
let host = node.view
|
||||
if host.superview !== self { self.addSubview(host) }
|
||||
node.updateLayout(size: iconSize)
|
||||
host.bounds = CGRect(origin: .zero, size: iconSize)
|
||||
host.layer.position = finalCenterInSelf
|
||||
|
||||
// Remove container
|
||||
container.removeFromSuperview()
|
||||
self.decelContainer = nil
|
||||
self.decelItemHosts.removeAll()
|
||||
|
||||
// Bounce scale (same values you already used)
|
||||
let factors: [CGFloat] = [1.0, 1.3, 0.92, 1.18, 0.98, 1.0]
|
||||
let values = factors.map { NSNumber(value: Double($0)) }
|
||||
let scaleAnim = CAKeyframeAnimation(keyPath: "transform.scale")
|
||||
scaleAnim.values = values
|
||||
scaleAnim.keyTimes = [0.0, 0.35, 0.55, 0.75, 0.9, 1.0].map(NSNumber.init)
|
||||
scaleAnim.timingFunctions = [
|
||||
CAMediaTimingFunction(name: .easeOut),
|
||||
CAMediaTimingFunction(name: .easeIn),
|
||||
CAMediaTimingFunction(name: .easeOut),
|
||||
CAMediaTimingFunction(name: .easeIn),
|
||||
CAMediaTimingFunction(name: .easeOut)
|
||||
]
|
||||
scaleAnim.duration = 0.85
|
||||
scaleAnim.isRemovedOnCompletion = true
|
||||
host.layer.add(scaleAnim, forKey: "bounce")
|
||||
|
||||
// Play the “once” sticker & finish
|
||||
node.playOnce()
|
||||
self.finishSettled()
|
||||
}
|
||||
|
||||
private func tick() {
|
||||
guard let component = self.component else { return }
|
||||
let now = CACurrentMediaTime()
|
||||
|
||||
switch self.spinState {
|
||||
case .spinning:
|
||||
if self.lastSpawnTime == nil || now - (self.lastSpawnTime ?? now) >= self.currentInterval {
|
||||
self.lastSpawnTime = now
|
||||
|
||||
guard !self.spinPool.isEmpty else { return }
|
||||
|
||||
if self.spinPoolIndex >= self.spinPool.count { self.spinPoolIndex = 0 }
|
||||
let next = self.spinPool[self.spinPoolIndex]
|
||||
self.spinPoolIndex += 1
|
||||
|
||||
self.spawnModelItem(next, animDuration: self.baseAnimDuration)
|
||||
}
|
||||
|
||||
var updateNeeded = false
|
||||
if self.lastPatternChangeTime == nil || now - (self.lastPatternChangeTime ?? now) >= self.currentInterval * 6.0 {
|
||||
self.lastPatternChangeTime = now
|
||||
|
||||
if component.revealedAttributes.contains(.pattern) {
|
||||
if self.previewPatternIndex != -1 {
|
||||
self.previewPatternIndex = -1
|
||||
self.animatePreviewTransition = true
|
||||
updateNeeded = true
|
||||
}
|
||||
} else {
|
||||
let previousPatternIndex = self.previewPatternIndex
|
||||
var randomPatternIndex = previousPatternIndex
|
||||
while randomPatternIndex == previousPatternIndex && !self.previewPatterns.isEmpty {
|
||||
randomPatternIndex = Int32.random(in: 0 ..< Int32(self.previewPatterns.count))
|
||||
}
|
||||
if !self.previewPatterns.isEmpty { self.previewPatternIndex = randomPatternIndex }
|
||||
|
||||
self.animatePreviewTransition = true
|
||||
updateNeeded = true
|
||||
}
|
||||
}
|
||||
if self.lastBackdropChangeTime == nil || now - (self.lastBackdropChangeTime ?? now) >= self.currentInterval * 3.55 {
|
||||
self.lastBackdropChangeTime = now
|
||||
|
||||
if component.revealedAttributes.contains(.backdrop) {
|
||||
if self.previewBackdropIndex != -1 {
|
||||
self.previewBackdropIndex = -1
|
||||
self.animateBackdropSwipe = true
|
||||
updateNeeded = true
|
||||
}
|
||||
} else {
|
||||
let previousBackdropIndex = self.previewBackdropIndex
|
||||
var randomBackdropIndex = previousBackdropIndex
|
||||
while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty {
|
||||
randomBackdropIndex = Int32.random(in: 0 ..< Int32(self.previewBackdrops.count))
|
||||
}
|
||||
if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex }
|
||||
|
||||
self.animateBackdropSwipe = true
|
||||
updateNeeded = true
|
||||
}
|
||||
}
|
||||
|
||||
if updateNeeded {
|
||||
self.componentState?.updated(transition: .easeInOut(duration: 0.25))
|
||||
self.component?.requestUpdate(.easeInOut(duration: 0.25))
|
||||
}
|
||||
case .decelerating:
|
||||
break
|
||||
case .idle, .settled:
|
||||
self.spinLink?.invalidate()
|
||||
self.spinLink = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ swift_library(
|
|||
"//submodules/ActivityIndicator",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
|
||||
"//submodules/ImageBlur",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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:
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "TimeLocked.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/TimeLocked.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/DateLockedIcon.imageset/TimeLocked.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import TelegramUniversalVideoContent
|
|||
import DeviceProximity
|
||||
import MediaResources
|
||||
import PhotoResources
|
||||
import PeerMessagesMediaPlaylist
|
||||
|
||||
enum SharedMediaPlayerGroup: Int {
|
||||
case music = 0
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import GalleryData
|
|||
import StoryContainerScreen
|
||||
import WallpaperGalleryScreen
|
||||
import BrowserUI
|
||||
import PeerMessagesMediaPlaylist
|
||||
|
||||
func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
var story: TelegramMediaStory?
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import TelegramAudio
|
|||
import AccountContext
|
||||
import TelegramUniversalVideoContent
|
||||
import DeviceProximity
|
||||
import PeerMessagesMediaPlaylist
|
||||
|
||||
private enum SharedMediaPlaybackItem: Equatable {
|
||||
case audio(MediaPlayer)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue