Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

# Conflicts:
#	submodules/MediaPlayer/Sources/MediaDataReader.swift
This commit is contained in:
Mikhail Filimonov 2024-12-28 11:29:44 +04:00
commit 04eb8524c8
337 changed files with 8318 additions and 3019 deletions

View file

@ -73,7 +73,9 @@ experimental_i:
except:
- tags
script:
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64
- export PATH=/opt/homebrew/opt/ruby/bin:$PATH
- export PATH=`gem environment gemdir`/bin:$PATH
- python3 -u build-system/Make/Make.py remote-build --darwinContainers="$DARWIN_CONTAINERS" --darwinContainersHost="$DARWIN_CONTAINERS_HOST" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=adhoc --configuration=release_arm64
- python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
environment:
name: experimental

View file

@ -593,8 +593,10 @@ private struct NotificationContent: CustomStringConvertible {
if !self.userInfo.isEmpty {
content.userInfo = self.userInfo
}
if !self.attachments.isEmpty {
content.attachments = self.attachments
if self.isLockedMessage == nil {
if !self.attachments.isEmpty {
content.attachments = self.attachments
}
}
if #available(iOS 15.0, *) {
@ -980,7 +982,7 @@ private final class NotificationServiceHandler {
enum Action {
case logout
case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?)
case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?, reportDelivery: Bool)
case pollStories(peerId: PeerId, content: NotificationContent, storyId: Int32, isReaction: Bool)
case deleteMessage([MessageId])
case readReactions([MessageId])
@ -999,7 +1001,7 @@ private final class NotificationServiceHandler {
action = .logout
case "MESSAGE_MUTED":
if let peerId = peerId {
action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil)
action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil, reportDelivery: false)
}
case "MESSAGE_DELETED":
if let peerId = peerId {
@ -1183,9 +1185,16 @@ private final class NotificationServiceHandler {
action = .pollStories(peerId: peerId, content: content, storyId: storyId, isReaction: isReaction)
} else {
action = .poll(peerId: peerId, content: content, messageId: messageIdValue)
var reportDelivery = false
if let reportDeliveryUntilDate = aps["report_delivery_until_date"] as? Int32, let messageId = messageIdValue {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if reportDeliveryUntilDate > currentTime {
reportDelivery = true
}
}
action = .poll(peerId: peerId, content: content, messageId: messageIdValue, reportDelivery: reportDelivery)
}
updateCurrentContent(content)
}
}
@ -1245,7 +1254,7 @@ private final class NotificationServiceHandler {
let content = NotificationContent(isLockedMessage: nil)
updateCurrentContent(content)
completed()
case let .poll(peerId, initialContent, messageId):
case let .poll(peerId, initialContent, messageId, reportDelivery):
Logger.shared.log("NotificationService \(episode)", "Will poll")
if let stateManager = strongSelf.stateManager {
let shouldKeepConnection = stateManager.network.shouldKeepConnection
@ -1683,12 +1692,23 @@ private final class NotificationServiceHandler {
pollWithUpdatedContent = pollSignal
|> map { _ -> (NotificationContent, Media?) in }
}
let reportDeliverySignal: Signal<Bool, NoError>
if reportDelivery, let messageId {
reportDeliverySignal = _internal_reportMessageDelivery(postbox: stateManager.postbox, network: stateManager.network, messageIds: [messageId], fromPushNotification: true)
|> mapToSignal { _ -> Signal<Bool, NoError> in
return .single(true)
}
|> then(.single(true))
} else {
reportDeliverySignal = .single(true)
}
var updatedContent = initialContent
var updatedMedia: Media?
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content, media in
updatedContent = content
updatedMedia = media
strongSelf.pollDisposable.set(combineLatest(pollWithUpdatedContent, reportDeliverySignal).start(next: { contentAndMedia, _ in
updatedContent = contentAndMedia.0
updatedMedia = contentAndMedia.1
}, completed: {
pollCompletion(updatedContent, updatedMedia)
}))
@ -1951,6 +1971,8 @@ private final class NotificationServiceHandler {
} else {
pollWithUpdatedContent = .complete()
}
var updatedContent = initialContent
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in

Binary file not shown.

View file

@ -12299,6 +12299,7 @@ Sorry for the inconvenience.";
"Stars.Intro.Transaction.ConvertedGift" = "Converted Gift";
"Stars.Intro.Transaction.Unsupported.Title" = "Unsupported";
"Stars.Intro.Transaction.Refund" = "Refund";
"Stars.Intro.Transaction.GiftUpgrade" = "Gift Upgrade";
"Stars.Intro.PurchasedTitle" = "Stars Acquired";
"Stars.Intro.PurchasedText" = "**%@** added to your balance.";
@ -13503,9 +13504,9 @@ Sorry for the inconvenience.";
"Gift.Unique.Availability" = "Availability";
"Gift.Unique.Issued" = "%@ issued";
"Gift.Unique.OriginalInfo" = "Gifted to %1$@ on %2$@.";
"Gift.Unique.OriginalInfoWithText" = "Gifted to %1$@ on %2$@ with comment \"%3$@\"";
"Gift.Unique.OriginalInfoWithText" = "Gifted to %1$@ on %2$@ with comment \"%3$@\".";
"Gift.Unique.OriginalInfoSender" = "Gifted by %1$@ to %2$@ on %3$@.";
"Gift.Unique.OriginalInfoSenderWithText" = "Gifted by %1$@ to %2$@ on %3$@ with comment \"%4$@\"";
"Gift.Unique.OriginalInfoSenderWithText" = "Gifted by %1$@ to %2$@ on %3$@ with comment \"%4$@\".";
"Gift.Unique.AttributeDescription" = "Only %@ of such collectibles have this attribute.";
@ -13539,6 +13540,20 @@ Sorry for the inconvenience.";
"PeerInfo.VerificationInfo.Group" = "This group is verified as official by the representatives of Telegram.";
"PeerInfo.VerificationInfo.URL" = "https://telegram.org/verify";
"ChatList.ToastFolderMutedV2" = "All chats in {folder} are now muted";
"ChatList.ToastFolderUnmutedV2" = "All chats in {folder} are now unmuted";
"ChatList.AddedToFolderTooltipV2" = "{chat} has been added to folder {folder}";
"ChatList.RemovedFromFolderTooltipV2" = "{chat} has been removed from folder {folder}";
"FolderLinkScreen.TitleDescriptionDeselectedV2" = "Anyone with this link can add {folder} folder and the chats selected below.";
"FolderLinkScreen.TitleDescriptionSelectedV2" = "Anyone with this link can add {folder} folder and {chats} selected below.";
"Chat.NextChannelFolderSwipeProgressV2" = "Swipe up to go to the {folder} folder";
"Chat.NextChannelFolderSwipeActionV2" = "Release to go to the {folder} folder";
"FolderLinkPreview.TextAddChatsV2" = "Do you want to add {chats} to the\nfolder {folder}?";
"FolderLinkPreview.ToastLeftTitleV2" = "Folder {folder} deleted";
"FolderLinkPreview.ToastChatsAddedTitleV2" = "Folder {folder} Updated";
"FolderLinkPreview.ToastFolderAddedTitleV2" = "Folder {folder} Added";
"Stars.Purchase.UpgradeStarGiftInfo" = "Buy Stars to upgrade yort gift into a unique collectible.";
"Gift.Send.Upgrade" = "Make Unique for %@";
@ -13561,4 +13576,14 @@ Sorry for the inconvenience.";
"Notification.StarGift.Subtitle.Refunded" = "This gift cannot be converted to Stars because the payment related to it was refunded.";
"Notification.StarGift.Subtitle.Downgraded" = "This gift was downgraded because a request to refund the payment related to this gift was made, and the money was returned.";
"Gift.View.KeepOrUpgradeDescription" = "You can keep this gift or upgrade it.";
"Notification.StarGift.Subtitle.Other" = "%1$@ can turn this gift to a unique collectible.";
"Stars.Transaction.GiftFrom" = "Gift From";
"Stars.Transaction.GiftUpgrade" = "Gift Upgrade";
"Gift.View.KeepOrUpgradeDescription" = "Tap \"Unpack\" to turn this gift to a unique collectible.";
"VideoChat.IncomingVideoQuality.AudioOnly" = "Audio Only";
"VideoChat.IncomingVideoQuality.Title" = "Receive Video Quality";
"ChatList.EmptyResult.SearchInAll" = "Search in All Messages";

View file

@ -413,7 +413,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case monospace
case textMention(EnginePeer.Id)
case textUrl(String)
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64)
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64, enableAnimation: Bool)
case strikethrough
case underline
case spoiler
@ -440,7 +440,8 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case 5:
let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s")
let fileId = try container.decode(Int64.self, forKey: "f")
self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
let enableAnimation = try container.decodeIfPresent(Bool.self, forKey: "ea") ?? true
self = .customEmoji(stickerPack: stickerPack, fileId: fileId, enableAnimation: enableAnimation)
case 6:
self = .strikethrough
case 7:
@ -474,10 +475,11 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
case let .textUrl(url):
try container.encode(4 as Int32, forKey: "t")
try container.encode(url, forKey: "url")
case let .customEmoji(stickerPack, fileId):
case let .customEmoji(stickerPack, fileId, enableAnimation):
try container.encode(5 as Int32, forKey: "t")
try container.encodeIfPresent(stickerPack, forKey: "s")
try container.encode(fileId, forKey: "f")
try container.encode(enableAnimation, forKey: "ea")
case .strikethrough:
try container.encode(6 as Int32, forKey: "t")
case .underline:
@ -560,7 +562,7 @@ public struct ChatTextInputStateText: Codable, Equatable {
} else if key == ChatTextInputAttributes.textUrl, let value = value as? ChatTextInputTextUrlAttribute {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length)))
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId, enableAnimation: value.enableAnimation), range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.strikethrough {
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length)))
} else if key == ChatTextInputAttributes.underline {
@ -618,8 +620,8 @@ public struct ChatTextInputStateText: Codable, Equatable {
result.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: id), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .textUrl(url):
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .customEmoji(_, fileId):
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case let .customEmoji(_, fileId, enableAnimation):
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil, enableAnimation: enableAnimation), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .strikethrough:
result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
case .underline:
@ -1204,3 +1206,40 @@ public protocol ChatHistoryListNode: ListView {
var contentPositionChanged: (ListViewVisibleContentOffset) -> Void { get set }
}
public extension ChatFolderTitle {
init(attributedString: NSAttributedString, enableAnimations: Bool) {
let inputStateText = ChatTextInputStateText(attributedText: attributedString)
self.init(text: inputStateText.text, entities: inputStateText.attributes.compactMap { attribute -> MessageTextEntity? in
if case let .customEmoji(_, fileId, _) = attribute.type {
return MessageTextEntity(range: attribute.range, type: .CustomEmoji(stickerPack: nil, fileId: fileId))
}
return nil
}, enableAnimations: enableAnimations)
}
var rawAttributedString: NSAttributedString {
let inputStateText = ChatTextInputStateText(text: self.text, attributes: self.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
if case let .CustomEmoji(_, fileId) = entity.type {
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: self.enableAnimations), range: entity.range)
}
return nil
})
return inputStateText.attributedText()
}
func attributedString(attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: self.rawAttributedString)
result.addAttributes(attributes, range: NSRange(location: 0, length: result.length))
return result
}
func attributedString(font: UIFont, textColor: UIColor) -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: self.rawAttributedString)
result.addAttributes([
.font: font,
.foregroundColor: textColor
], range: NSRange(location: 0, length: result.length))
return result
}
}

View file

@ -146,7 +146,7 @@ public protocol MediaManager: AnyObject {
var musicMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { get }
var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> { get }
func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction)
func setPlaylist(_ playlist: (AccountContext, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction)
func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?)
func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal<SharedMediaPlayerItemPlaybackState?, NoError>

View file

@ -53,7 +53,7 @@ public protocol UniversalVideoContent {
var dimensions: CGSize { get }
var duration: Double { get }
func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
func isEqual(to other: UniversalVideoContent) -> Bool
}
@ -95,7 +95,7 @@ public enum UniversalVideoNodeFetchControl {
}
public final class UniversalVideoNode: ASDisplayNode {
private let accountId: AccountRecordId
private let context: AccountContext
private let postbox: Postbox
private let audioSession: ManagedAudioSession
private let manager: UniversalVideoManager
@ -146,12 +146,12 @@ public final class UniversalVideoNode: ASDisplayNode {
if self.canAttachContent {
assert(self.contentRequestIndex == nil)
let accountId = self.accountId
let context = self.context
let content = self.content
let postbox = self.postbox
let audioSession = self.audioSession
self.contentRequestIndex = self.manager.attachUniversalVideoContent(content: self.content, priority: self.priority, create: {
return content.makeContentNode(accountId: accountId, postbox: postbox, audioSession: audioSession)
return content.makeContentNode(context: context, postbox: postbox, audioSession: audioSession)
}, update: { [weak self] contentNodeAndFlags in
if let strongSelf = self {
strongSelf.updateContentNode(contentNodeAndFlags)
@ -172,8 +172,8 @@ public final class UniversalVideoNode: ASDisplayNode {
return self.contentNode != nil
}
public init(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) {
self.accountId = accountId
public init(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) {
self.context = context
self.postbox = postbox
self.audioSession = audioSession
self.manager = manager

View file

@ -218,7 +218,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
public var isPlaying: Bool = false
private var currentLoopCount: Int = 0
private var canDisplayFirstFrame: Bool = false
private var playbackMode: AnimatedStickerPlaybackMode = .loop
public var playbackMode: AnimatedStickerPlaybackMode = .loop
public var stopAtNearestLoop: Bool = false

View file

@ -234,7 +234,7 @@ public final class AvatarVideoNode: ASDisplayNode {
if self.videoNode == nil {
let context = self.context
let mediaManager = context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
videoNode.clipsToBounds = true
videoNode.isUserInteractionEnabled = false
videoNode.isHidden = true

View file

@ -1527,7 +1527,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}
switch result {
case let .done(receiptMessageId, _):
case let .done(receiptMessageId, _, _):
proceedWithCompletion(true, receiptMessageId)
case let .externalVerificationRequired(url):
strongSelf.updateActionButton()

View file

@ -1076,7 +1076,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
}
}
self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
self.context.sharedContext.mediaManager.setPlaylist((self.context, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
return
}

View file

@ -464,7 +464,7 @@ public final class ChatImportActivityScreen: ViewController {
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
videoNode.alpha = 0.01
self.videoNode = videoNode

View file

@ -103,6 +103,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/TextFieldComponent",
@ -110,6 +111,7 @@ swift_library(
"//submodules/ComposePollUI",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUI/Components/LottieComponent",
],
visibility = [
"//visibility:public",

View file

@ -223,8 +223,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
|> deliverOnMainQueue).startStandalone(completed: {
c?.dismiss(completion: {
//TODO:release
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
@ -274,8 +273,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
let filterType = chatListFilterType(data)
//TODO:release
updatedItems.append(.action(ContextMenuActionItem(text: title.text, icon: { theme in
updatedItems.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, icon: { theme in
let imageName: String
switch filterType {
case .generic:
@ -339,8 +337,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
return filters
}).startStandalone()
//TODO:release
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
chatListController?.present(UndoOverlayController( presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
return false
}), in: .current)
})
@ -348,7 +345,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
}
}
c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true)
c?.setItems(.single(ContextController.Items(content: .list(updatedItems), context: context)), minHeight: nil, animated: true)
})))
items.append(.separator)
}

View file

@ -51,6 +51,7 @@ import PeerInfoStoryGridScreen
import ArchiveInfoScreen
import BirthdayPickerScreen
import OldChannelsController
import TextFormat
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
@ -1453,7 +1454,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
let contextController = ContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
dismissPreviewingImpl = { [weak contextController] in
@ -1526,7 +1527,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
let contextController = ContextController(presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
let contextController = ContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
}
@ -1811,28 +1812,42 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
//TODO:release
let iconColor: UIColor = .white
let overlayController: UndoOverlayController
if !filterPeersAreMuted.areMuted {
let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title.text).string
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
let text = NSMutableAttributedString(string: strongSelf.presentationData.strings.ChatList_ToastFolderMutedV2)
let folderNameRange = (text.string as NSString).range(of: "{folder}")
if folderNameRange.location != NSNotFound {
text.replaceCharacters(in: folderNameRange, with: "")
text.insert(title.attributedString(attributes: [
ChatTextInputAttributes.bold: true
]), at: folderNameRange.location)
}
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universalWithEntities(context: strongSelf.context, animation: "anim_profilemute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
], title: nil, text: text, animateEntities: title.enableAnimations, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
} else {
//TODO:release
let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title.text).string
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
let text = NSMutableAttributedString(string: strongSelf.presentationData.strings.ChatList_ToastFolderUnmutedV2)
let folderNameRange = (text.string as NSString).range(of: "{folder}")
if folderNameRange.location != NSNotFound {
text.replaceCharacters(in: folderNameRange, with: "")
text.insert(title.attributedString(attributes: [
ChatTextInputAttributes.bold: true
]), at: folderNameRange.location)
}
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universalWithEntities(context: strongSelf.context, animation: "anim_profileunmute", scale: 0.075, colors: [
"Middle.Group 1.Fill 1": iconColor,
"Top.Group 1.Fill 1": iconColor,
"Bottom.Group 1.Fill 1": iconColor,
"EXAMPLE.Group 1.Fill 1": iconColor,
"Line.Group 1.Stroke 1": iconColor
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
], title: nil, text: text, animateEntities: title.enableAnimations, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
}
strongSelf.present(overlayController, in: .current)
})
@ -4736,7 +4751,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let text = strongSelf.presentationData.strings.ChatList_DeletedThreads(Int32(threadIds.count))
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
}
@ -4842,7 +4857,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
}
@ -4914,7 +4929,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let text = strongSelf.presentationData.strings.ChatList_DeletedChats(Int32(peerIds.count))
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
}
@ -5274,7 +5289,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return true
})
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: strongSelf.presentationData.strings.Undo_ChatCleared, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: strongSelf.context, title: NSAttributedString(string: strongSelf.presentationData.strings.Undo_ChatCleared), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { value in
guard let strongSelf = self else {
return false
}
@ -5496,7 +5511,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let statusText = self.presentationData.strings.Undo_DeletedTopic
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let self else {
return false
}
@ -5793,7 +5808,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return true
})
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: statusText), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let strongSelf = self else {
return false
}
@ -5933,7 +5948,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive)
}
}
//TODO:release
items.append(.action(ContextMenuActionItem(text: title.text, entities: title.entities, enableEntityAnimations: title.enableAnimations, badge: badge, icon: { theme in
let imageName: String
if isDisabled {
@ -6347,9 +6361,9 @@ private final class ChatListLocationContext {
let peerView = Promise<PeerView>()
peerView.set(context.account.viewTracker.peerView(peerId))
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
var onlineMemberCount: Signal<(total: Int32?, recent: Int32?), NoError> = .single((nil, nil))
let recentOnlineSignal: Signal<Int32?, NoError> = peerView.get()
let recentOnlineSignal: Signal<(total: Int32?, recent: Int32?), NoError> = peerView.get()
|> map { view -> Bool? in
if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel {
if case .broadcast = peer.info {
@ -6364,17 +6378,21 @@ private final class ChatListLocationContext {
}
}
|> distinctUntilChanged
|> mapToSignal { isLarge -> Signal<Int32?, NoError> in
|> mapToSignal { isLarge -> Signal<(total: Int32?, recent: Int32?), NoError> in
if let isLarge = isLarge {
if isLarge {
return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId)
|> map(Optional.init)
|> map { value -> (total: Int32?, recent: Int32?) in
return (nil, value)
}
} else {
return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId)
|> map(Optional.init)
|> map { value -> (total: Int32?, recent: Int32?) in
return (value.total, value.recent)
}
}
} else {
return .single(nil)
return .single((nil, nil))
}
}
onlineMemberCount = recentOnlineSignal
@ -6783,7 +6801,7 @@ private final class ChatListLocationContext {
private func updateForum(
peerId: EnginePeer.Id,
peerView: PeerView,
onlineMemberCount: Int32?,
onlineMemberCount: (total: Int32?, recent: Int32?),
stateAndFilterId: (state: ChatListNodeState, filterId: Int32?),
presentationData: PresentationData
) {

View file

@ -35,6 +35,7 @@ private final class ChatListFilterPresetControllerArguments {
let context: AccountContext
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
let updateName: (ChatFolderTitle) -> Void
let toggleNameInputMode: () -> Void
let toggleNameAnimations: () -> Void
let openAddIncludePeer: () -> Void
let openAddExcludePeer: () -> Void
@ -58,6 +59,7 @@ private final class ChatListFilterPresetControllerArguments {
context: AccountContext,
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
updateName: @escaping (ChatFolderTitle) -> Void,
toggleNameInputMode: @escaping () -> Void,
toggleNameAnimations: @escaping () -> Void,
openAddIncludePeer: @escaping () -> Void,
openAddExcludePeer: @escaping () -> Void,
@ -80,6 +82,7 @@ private final class ChatListFilterPresetControllerArguments {
self.context = context
self.updateState = updateState
self.updateName = updateName
self.toggleNameInputMode = toggleNameInputMode
self.toggleNameAnimations = toggleNameAnimations
self.openAddIncludePeer = openAddIncludePeer
self.openAddExcludePeer = openAddExcludePeer
@ -228,7 +231,7 @@ private enum ChatListFilterRevealedItemId: Equatable {
private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case screenHeader
case nameHeader(title: String, enableAnimations: Bool)
case nameHeader(title: String, enableAnimations: Bool?)
case name(placeholder: String, value: NSAttributedString, inputMode: ListComposePollOptionComponent.InputMode?, enableAnimations: Bool)
case includePeersHeader(String)
case addIncludePeer(title: String)
@ -376,7 +379,11 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .newFolder, sectionId: self.section)
case let .nameHeader(title, enableAnimations):
//TODO:localize
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: enableAnimations ? "Disable Animations" : "Enable Animations", action: {
var actionText: String?
if let enableAnimations {
actionText = enableAnimations ? "Disable Animations" : "Enable Animations"
}
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, actionText: actionText, action: {
arguments.toggleNameAnimations()
}, sectionId: self.section)
case let .name(placeholder, value, inputMode, enableAnimations):
@ -393,15 +400,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
arguments.updateName(ChatFolderTitle(attributedString: value, enableAnimations: true))
},
toggleInputMode: {
arguments.updateState { current in
var state = current
if state.nameInputMode == .emoji {
state.nameInputMode = .keyboard
} else {
state.nameInputMode = .emoji
}
return state
}
arguments.toggleNameInputMode()
}
)
case .includePeersHeader(let text), .excludePeersHeader(let text):
@ -545,37 +544,6 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
}
}
extension ChatFolderTitle {
init(attributedString: NSAttributedString, enableAnimations: Bool) {
let inputStateText = ChatTextInputStateText(attributedText: attributedString)
self.init(text: inputStateText.text, entities: inputStateText.attributes.compactMap { attribute -> MessageTextEntity? in
if case let .customEmoji(_, fileId) = attribute.type {
return MessageTextEntity(range: attribute.range, type: .CustomEmoji(stickerPack: nil, fileId: fileId))
}
return nil
}, enableAnimations: enableAnimations)
}
var rawAttributedString: NSAttributedString {
let inputStateText = ChatTextInputStateText(text: self.text, attributes: self.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
if case let .CustomEmoji(_, fileId) = entity.type {
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range)
}
return nil
})
return inputStateText.attributedText()
}
func attributedString(font: UIFont, textColor: UIColor) -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: self.rawAttributedString)
result.addAttributes([
.font: font,
.foregroundColor: textColor
], range: NSRange(location: 0, length: result.length))
return result
}
}
private struct ChatListFilterPresetControllerState: Equatable {
var name: ChatFolderTitle
var changedName: Bool
@ -624,7 +592,7 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
entries.append(.screenHeader)
}
entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.enableAnimations))
entries.append(.nameHeader(title: presentationData.strings.ChatListFolder_NameSectionHeader, enableAnimations: state.name.entities.isEmpty ? nil : state.name.enableAnimations))
entries.append(.name(placeholder: presentationData.strings.ChatListFolder_NamePlaceholder, value: state.name.rawAttributedString, inputMode: state.nameInputMode, enableAnimations: state.name.enableAnimations))
entries.append(.includePeersHeader(presentationData.strings.ChatListFolder_IncludedSectionHeader))
@ -1584,6 +1552,18 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
}
}
},
toggleNameInputMode: {
updateState { current in
var state = current
if state.nameInputMode == .emoji {
state.nameInputMode = .keyboard
} else {
state.nameInputMode = .emoji
}
return state
}
focusOnNameImpl?()
},
toggleNameAnimations: {
updateState { current in
var name = current.name
@ -2145,7 +2125,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
return
}
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode {
if let itemNode = itemNode as? ItemListFilterTitleInputItemNode {
itemNode.focus()
}
}

View file

@ -16,7 +16,7 @@ import ChatFolderLinkPreviewScreen
private final class ChatListFilterPresetListControllerArguments {
let context: AccountContext
let addSuggestedPressed: (String, ChatListFilterData) -> Void
let addSuggestedPressed: (ChatFolderTitle, ChatListFilterData) -> Void
let openPreset: (ChatListFilter) -> Void
let addNew: () -> Void
let setItemWithRevealedOptions: (Int32?, Int32?) -> Void
@ -24,7 +24,7 @@ private final class ChatListFilterPresetListControllerArguments {
let updateDisplayTags: (Bool) -> Void
let updateDisplayTagsLocked: () -> Void
init(context: AccountContext, addSuggestedPressed: @escaping (String, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) {
init(context: AccountContext, addSuggestedPressed: @escaping (ChatFolderTitle, ChatListFilterData) -> Void, openPreset: @escaping (ChatListFilter) -> Void, addNew: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, removePreset: @escaping (Int32) -> Void, updateDisplayTags: @escaping (Bool) -> Void, updateDisplayTagsLocked: @escaping () -> Void) {
self.context = context
self.addSuggestedPressed = addSuggestedPressed
self.openPreset = openPreset
@ -92,10 +92,10 @@ private struct PresetIndex: Equatable {
private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case screenHeader(String)
case suggestedListHeader(String)
case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData)
case suggestedPreset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilterData)
case suggestedAddCustom(String)
case listHeader(String)
case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool)
case preset(index: PresetIndex, title: ChatFolderTitle, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool, displayTags: Bool)
case addItem(text: String, isEditing: Bool)
case listFooter(String)
case displayTags(Bool?)
@ -176,7 +176,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
case let .suggestedListHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .suggestedPreset(_, title, label, preset):
return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title, label: label, sectionId: self.section, style: .blocks, installAction: {
return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: {
arguments.addSuggestedPressed(title, preset)
}, tag: nil)
case let .suggestedAddCustom(text):
@ -194,7 +194,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
}
}
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: {
return ChatListFilterPresetListItem(context: arguments.context, presentationData: presentationData, preset: preset, title: title, label: label, tagColor: resolvedColor, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: {
if isDisabled {
arguments.addNew()
} else {
@ -285,12 +285,11 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
var folderCount = 0
for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) {
if case .allChats = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true))
entries.append(.preset(index: PresetIndex(value: entries.count), title: ChatFolderTitle(text: "", entities: [], enableAnimations: true), label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false, displayTags: effectiveDisplayTags == true))
}
if case let .filter(_, title, _, _) = filter {
folderCount += 1
//TODO:release
entries.append(.preset(index: PresetIndex(value: entries.count), title: title.text, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true))
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount, displayTags: effectiveDisplayTags == true))
}
}
@ -300,8 +299,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
if !filteredSuggestedFilters.isEmpty && actualFilters.count < limits.maxFoldersCount {
entries.append(.suggestedListHeader(presentationData.strings.ChatListFolderSettings_RecommendedFoldersSection))
for filter in filteredSuggestedFilters {
//TODO:release
entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title.text, label: filter.description, preset: filter.data))
entries.append(.suggestedPreset(index: PresetIndex(value: entries.count), title: filter.title, label: filter.description, preset: filter.data))
}
if filters.isEmpty {
entries.append(.suggestedAddCustom(presentationData.strings.ChatListFolderSettings_RecommendedNewFolder))
@ -389,8 +387,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
let id = context.engine.peers.generateNewChatListFilterId(filters: filters)
//TODO:release
filters.append(.filter(id: id, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), emoticon: nil, data: data))
filters.append(.filter(id: id, title: title, emoticon: nil, data: data))
return filters
}
|> deliverOnMainQueue).start(next: { _ in

View file

@ -7,6 +7,8 @@ import TelegramCore
import TelegramPresentationData
import ItemListUI
import TelegramUIPreferences
import AccountContext
import TextNodeWithEntities
struct ChatListFilterPresetListItemEditing: Equatable {
let editable: Bool
@ -15,9 +17,10 @@ struct ChatListFilterPresetListItemEditing: Equatable {
}
final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let preset: ChatListFilter
let title: String
let title: ChatFolderTitle
let label: String
let tagColor: UIColor?
let editing: ChatListFilterPresetListItemEditing
@ -31,9 +34,10 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
let remove: () -> Void
init(
context: AccountContext,
presentationData: ItemListPresentationData,
preset: ChatListFilter,
title: String,
title: ChatFolderTitle,
label: String,
tagColor: UIColor?,
editing: ChatListFilterPresetListItemEditing,
@ -46,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem {
setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void,
remove: @escaping () -> Void
) {
self.context = context
self.presentationData = presentationData
self.preset = preset
self.title = title
@ -124,7 +129,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
return self.containerNode
}
private let titleNode: TextNode
private let titleNode: TextNodeWithEntities
private let labelNode: TextNode
private let arrowNode: ASImageNode
private let sharedIconNode: ASImageNode
@ -145,6 +150,15 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
return true
}
override var visibility: ListViewItemNodeVisibility {
didSet {
if self.visibility != oldValue {
let enableAnimations = self.item?.title.enableAnimations ?? true
self.titleNode.visibilityRect = (self.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite
}
}
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
@ -160,10 +174,11 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.titleNode = TextNodeWithEntities()
self.titleNode.textNode.isUserInteractionEnabled = false
self.titleNode.textNode.contentMode = .left
self.titleNode.textNode.contentsScale = UIScreen.main.scale
self.titleNode.resetEmojiToFirstFrameAutomatically = true
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
@ -186,7 +201,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.titleNode.textNode)
self.containerNode.addSubnode(self.labelNode)
self.containerNode.addSubnode(self.arrowNode)
self.containerNode.addSubnode(self.sharedIconNode)
@ -199,7 +214,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
}
func asyncLayout() -> (_ item: ChatListFilterPresetListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
@ -230,7 +245,11 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
}
let titleAttributedString = NSMutableAttributedString()
titleAttributedString.append(NSAttributedString(string: item.isAllChats ? item.presentationData.strings.ChatList_FolderAllChats : item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
if item.isAllChats {
titleAttributedString.append(NSAttributedString(string: item.presentationData.strings.ChatList_FolderAllChats, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
} else {
titleAttributedString.append(item.title.attributedString(font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor))
}
var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)?
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
@ -337,9 +356,18 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
}
strongSelf.editableControlNode?.isHidden = !item.canBeDeleted
let _ = titleApply()
let _ = titleApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
))
let _ = labelApply()
let enableAnimations = item.title.enableAnimations
strongSelf.titleNode.visibilityRect = (strongSelf.visibility == ListViewItemNodeVisibility.none || !enableAnimations) ? CGRect.zero : CGRect.infinite
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
@ -385,7 +413,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size))
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 11.0), size: titleLayout.size))
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: 11.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
@ -542,7 +570,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
editingOffset = 0.0
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.textNode.frame.minY), size: self.titleNode.textNode.bounds.size))
var labelFrame = self.labelNode.frame
labelFrame.origin.x = params.width - rightArrowInset - labelFrame.width + revealOffset

View file

@ -109,11 +109,13 @@ private final class ItemNode: ASDisplayNode {
self.titleNode = ImmediateTextNodeWithEntities()
self.titleNode.displaysAsynchronously = false
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.titleNode.resetEmojiToFirstFrameAutomatically = true
self.titleActiveNode = ImmediateTextNodeWithEntities()
self.titleActiveNode.displaysAsynchronously = false
self.titleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.titleActiveNode.alpha = 0.0
self.titleActiveNode.resetEmojiToFirstFrameAutomatically = true
self.shortTitleContainer = ASDisplayNode()
@ -121,12 +123,14 @@ private final class ItemNode: ASDisplayNode {
self.shortTitleNode.displaysAsynchronously = false
self.shortTitleNode.alpha = 0.0
self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.shortTitleNode.resetEmojiToFirstFrameAutomatically = true
self.shortTitleActiveNode = ImmediateTextNodeWithEntities()
self.shortTitleActiveNode.displaysAsynchronously = false
self.shortTitleActiveNode.alpha = 0.0
self.shortTitleActiveNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
self.shortTitleActiveNode.alpha = 0.0
self.shortTitleActiveNode.resetEmojiToFirstFrameAutomatically = true
self.badgeContainerNode = ASDisplayNode()

View file

@ -239,7 +239,8 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode {
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
attemptSynchronous: true,
emojiOffset: CGPoint(x: 0.0, y: -1.0)
))
let badgeSideInset: CGFloat = 4.0
let badgeBackgroundSize: CGSize
@ -248,7 +249,7 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode {
} else {
badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
}
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize)
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize)
let badgeBackgroundLayer: SimpleLayer
if let current = strongSelf.badgeBackgroundLayer {
@ -262,6 +263,7 @@ public class ChatListFilterTagSectionHeaderItemNode: ListViewItemNode {
if strongSelf.badgeTextNode !== badgeTextNode {
strongSelf.badgeTextNode?.textNode.removeFromSupernode()
strongSelf.badgeTextNode = badgeTextNode
badgeTextNode.resetEmojiToFirstFrameAutomatically = true
strongSelf.addSubnode(badgeTextNode.textNode)
}

View file

@ -197,12 +197,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
} else if case let .user(user) = primaryPeer {
let servicePeer = isServicePeer(primaryPeer._asPeer())
if user.flags.contains(.isSupport) && !servicePeer {
status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Bot_GenericSupportStatus), multiline: false, isActive: false, icon: nil)
} else if let _ = user.botInfo {
if let subscriberCount = user.subscriberCount {
status = .custom(string: strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusBotSubscribers(subscriberCount)), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Bot_GenericBotStatus), multiline: false, isActive: false, icon: nil)
}
} else if user.id != context.account.peerId && !servicePeer {
let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
@ -211,19 +211,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
status = .none
}
} else if case let .legacyGroup(group) = primaryPeer {
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil)
} else if case let .channel(channel) = primaryPeer {
if case .group = channel.info {
if let count = peer.subpeerSummary?.count, count > 0 {
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.GroupInfo_ParticipantCount(Int32(count))), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Group_Status), multiline: false, isActive: false, icon: nil)
}
} else {
if let count = peer.subpeerSummary?.count, count > 0 {
status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusSubscribers(Int32(count))), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Channel_Status), multiline: false, isActive: false, icon: nil)
}
}
} else {
@ -380,6 +380,7 @@ public enum ChatListSearchEntryStableId: Hashable {
case globalPeerId(EnginePeer.Id)
case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
case messagePlaceholder(Int32)
case emptyMessagesFooter
case addContact
}
@ -442,6 +443,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool, TelegramSearchPeersScope)
case messagePlaceholder(Int32, ChatListPresentationData, TelegramSearchPeersScope)
case emptyMessagesFooter(ChatListPresentationData, TelegramSearchPeersScope, String?)
case addContact(String, PresentationTheme, PresentationStrings)
public var stableId: ChatListSearchEntryStableId {
@ -458,6 +460,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .messageId(message.id, section)
case let .messagePlaceholder(index, _, _):
return .messagePlaceholder(index)
case .emptyMessagesFooter:
return .emptyMessagesFooter
case .addContact:
return .addContact
}
@ -561,6 +565,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else {
return false
}
case let .emptyMessagesFooter(lhsPresentationData, lhsSearchScope, lhsQuery):
if case let .emptyMessagesFooter(rhsPresentationData, rhsSearchScope, rhsQuery) = rhs {
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsSearchScope != rhsSearchScope {
return false
}
if lhsQuery != rhsQuery {
return false
}
return true
} else {
return false
}
case let .addContact(lhsPhoneNumber, lhsTheme, lhsStrings):
if case let .addContact(rhsPhoneNumber, rhsTheme, rhsStrings) = rhs {
if lhsPhoneNumber != rhsPhoneNumber {
@ -601,7 +620,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return false
case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
case .globalPeer, .message, .messagePlaceholder, .addContact:
case .globalPeer, .message, .messagePlaceholder, .emptyMessagesFooter, .addContact:
return true
}
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
@ -610,7 +629,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
case .message, .messagePlaceholder, .addContact:
case .message, .messagePlaceholder, .emptyMessagesFooter, .addContact:
return true
}
case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _, _, _, _):
@ -618,6 +637,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return lhsKey < rhsKey
} else if case .messagePlaceholder = rhs {
return true
} else if case .emptyMessagesFooter = rhs {
return true
} else if case .addContact = rhs {
return true
} else {
@ -626,11 +647,19 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case let .messagePlaceholder(lhsIndex, _, _):
if case let .messagePlaceholder(rhsIndex, _, _) = rhs {
return lhsIndex < rhsIndex
} else if case .emptyMessagesFooter = rhs {
return true
} else if case .addContact = rhs {
return true
} else {
return false
}
case .emptyMessagesFooter:
if case .addContact = rhs {
return true
} else {
return false
}
case .addContact:
return false
}
@ -658,7 +687,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
toggleAllPaused: @escaping () -> Void,
openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void,
openPublicPosts: @escaping () -> Void,
openMessagesFilter: @escaping (ASDisplayNode) -> Void
openMessagesFilter: @escaping (ASDisplayNode) -> Void,
switchMessagesFilter: @escaping (TelegramSearchPeersScope) -> Void
) -> ListViewItem {
switch self {
case let .topic(peer, threadInfo, _, theme, strings, expandType):
@ -870,9 +900,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var status: ContactsPeerItemStatus = .none
if case let .user(user) = primaryPeer, let _ = user.botInfo, !primaryPeer.id.isVerificationCodes {
if let subscriberCount = user.subscriberCount {
status = .custom(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount)), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: presentationData.strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: presentationData.strings.Bot_GenericBotStatus), multiline: false, isActive: false, icon: nil)
}
}
@ -1128,6 +1158,33 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openMessagesFilter(sourceNode)
})
return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: 0), timestamp: 0))), content: .loading, editing: false, hasActiveRevealControls: false, selected: false, header: header, enableContextActions: false, hiddenOffset: false, interaction: interaction)
case let .emptyMessagesFooter(presentationData, searchScope, searchQuery):
var actionTitle: String?
let filterTitle: String
switch searchScope {
case .everywhere:
filterTitle = presentationData.strings.ChatList_Search_Messages_AllChats
case .channels:
filterTitle = presentationData.strings.ChatList_Search_Messages_Channels
case .groups:
filterTitle = presentationData.strings.ChatList_Search_Messages_GroupChats
case .privateChats:
filterTitle = presentationData.strings.ChatList_Search_Messages_PrivateChats
}
actionTitle = "\(filterTitle) <"
let header = ChatListSearchItemHeader(type: .messages(location: nil), theme: presentationData.theme, strings: presentationData.strings, actionTitle: actionTitle, action: { sourceNode in
openMessagesFilter(sourceNode)
})
return ChatListSearchEmptyFooterItem(
theme: presentationData.theme,
strings: presentationData.strings,
header: header,
searchQuery: searchQuery,
searchAllMessages: searchScope == .everywhere ? nil : {
switchMessagesFilter(.everywhere)
}
)
case let .addContact(phoneNumber, theme, strings):
return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
interaction.addContact(phoneNumber)
@ -1201,12 +1258,42 @@ private func chatListSearchContainerPreparedRecentTransition(
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty)
}
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void, openMessagesFilter: @escaping (ASDisplayNode) -> Void) -> ChatListSearchContainerTransition {
public func chatListSearchContainerPreparedTransition(
from fromEntries: [ChatListSearchEntry],
to toEntries: [ChatListSearchEntry],
displayingResults: Bool,
isEmpty: Bool,
isLoading: Bool,
animated: Bool,
context: AccountContext,
presentationData: PresentationData,
enableHeaders: Bool,
filter: ChatListNodePeersFilter,
requestPeerType: [ReplyMarkupButtonRequestPeerType]?,
location: ChatListControllerLocation,
key: ChatListSearchPaneKey,
tagMask: EngineMessage.Tags?,
interaction: ChatListNodeInteraction,
listInteraction: ListMessageItemInteraction,
peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?,
toggleExpandLocalResults: @escaping () -> Void,
toggleExpandGlobalResults: @escaping () -> Void,
searchPeer: @escaping (EnginePeer) -> Void,
searchQuery: String?,
searchOptions: ChatListSearchOptions?,
messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?,
openClearRecentlyDownloaded: @escaping () -> Void,
toggleAllPaused: @escaping () -> Void,
openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void,
openPublicPosts: @escaping () -> Void,
openMessagesFilter: @escaping (ASDisplayNode) -> Void,
switchMessagesFilter: @escaping (TelegramSearchPeersScope) -> Void
) -> ChatListSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts, openMessagesFilter: openMessagesFilter, switchMessagesFilter: switchMessagesFilter), directionHint: nil) }
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
}
@ -2912,6 +2999,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
index += 1
}
} else {
var hasAnyMessages = false
for foundRemoteMessageSet in foundRemoteMessages.0 {
for message in foundRemoteMessageSet.messages {
if existingMessageIds.contains(message.id) {
@ -2936,10 +3024,22 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
//TODO:requiresPremiumForMessaging
hasAnyMessages = true
entries.append(.message(message, peer, foundRemoteMessageSet.readCounters[message.id.peerId], foundRemoteMessageSet.threadsData[message.id]?.info, presentationData, foundRemoteMessageSet.totalCount, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false, nil, false, searchScope))
index += 1
}
}
if !hasAnyMessages {
switch searchScope {
case .everywhere:
break
default:
if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_empty_search_footer"] != nil {
} else {
entries.append(.emptyMessagesFooter(presentationData, searchScope, query))
}
}
}
}
}
@ -3444,6 +3544,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.interaction.switchToFilter(.publicPosts)
}, openMessagesFilter: { sourceNode in
strongSelf.openMessagesFilter(sourceNode: sourceNode)
}, switchMessagesFilter: { filter in
strongSelf.searchScopePromise.set(.everywhere)
})
strongSelf.currentEntries = newEntries
if strongSelf.key == .downloads {

View file

@ -238,6 +238,7 @@ public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDele
backspaceKeyAction: nil,
selection: nil,
inputMode: item.inputMode,
alwaysDisplayInputModeSelector: true,
toggleInputMode: { [weak self] in
guard let self else {
return
@ -268,6 +269,9 @@ public class ItemListFilterTitleInputItemNode: ListViewItemNode, UITextFieldDele
}
public func focus() {
if let textFieldView = self.textField.view as? ListComposePollOptionComponent.View {
textFieldView.activateInput()
}
}
public func selectAll() {

View file

@ -4,6 +4,8 @@ import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ComponentFlow
import LottieComponent
class ChatListHoleItem: ListViewItem {
let theme: PresentationTheme
@ -78,3 +80,252 @@ class ChatListHoleItemNode: ListViewItemNode {
}
}
}
class ChatListSearchEmptyFooterItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let searchQuery: String?
let searchAllMessages: (() -> Void)?
let header: ListViewItemHeader?
let selectable: Bool = false
init(theme: PresentationTheme, strings: PresentationStrings, header: ListViewItemHeader?, searchQuery: String?, searchAllMessages: (() -> Void)?) {
self.theme = theme
self.strings = strings
self.header = header
self.searchQuery = searchQuery
self.searchAllMessages = searchAllMessages
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatListSearchEmptyFooterItemNode()
let (layout, apply) = node.asyncLayout()(self, params)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ChatListSearchEmptyFooterItemNode)
if let nodeValue = node() as? ChatListSearchEmptyFooterItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply()
})
}
}
}
}
}
}
class ChatListSearchEmptyFooterItemNode: ListViewItemNode {
private let contentNode: ASDisplayNode
private let titleNode: TextNode
private let textNode: TextNode
private let searchAllMessagesButton: HighlightableButtonNode
private let searchAllMessagesTitle: TextNode
private let icon = ComponentView<Empty>()
private var item: ChatListSearchEmptyFooterItem?
required init() {
self.contentNode = ASDisplayNode()
self.titleNode = TextNode()
self.textNode = TextNode()
self.searchAllMessagesButton = HighlightableButtonNode()
self.searchAllMessagesTitle = TextNode()
self.searchAllMessagesTitle.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.titleNode)
self.contentNode.addSubnode(self.textNode)
self.contentNode.addSubnode(self.searchAllMessagesButton)
self.searchAllMessagesButton.addSubnode(self.searchAllMessagesTitle)
self.searchAllMessagesButton.addTarget(self, action: #selector(self.searchAllMessagesButtonPressed), forControlEvents: .touchUpInside)
self.wantsTrailingItemSpaceUpdates = true
}
@objc private func searchAllMessagesButtonPressed() {
self.item?.searchAllMessages?()
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (_, apply) = layout(item as! ChatListSearchEmptyFooterItem, params)
apply()
}
override func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
override func updateTrailingItemSpace(_ trailingItemSpace: CGFloat, transition: ContainedViewLayoutTransition) {
var contentFrame = self.contentNode.frame
contentFrame.origin.y = max(0.0, floor(trailingItemSpace * 0.5))
self.contentNode.frame = contentFrame
}
func asyncLayout() -> (_ item: ChatListSearchEmptyFooterItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleNodeLayout = TextNode.asyncLayout(self.titleNode)
let makeTextNodeLayout = TextNode.asyncLayout(self.textNode)
let makeSearchAllMessagesTitleLayout = TextNode.asyncLayout(self.searchAllMessagesTitle)
return { [weak self] item, params in
let titleLayout = makeTitleNodeLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: item.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: item.theme.list.freeTextColor),
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0)
))
let textValue: String
if let searchQuery = item.searchQuery {
textValue = item.strings.ChatList_Search_NoResultsQueryDescription(searchQuery).string
} else {
textValue = item.strings.ChatList_Search_NoResults
}
let textLayout = makeTextNodeLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: textValue, font: Font.regular(16.0), textColor: item.theme.list.freeTextColor),
maximumNumberOfLines: 0,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0),
alignment: .center,
lineSpacing: 0.1
))
let searchAllMessagesTitleLayout = makeSearchAllMessagesTitleLayout(TextNodeLayoutArguments(
attributedString: NSAttributedString(string: item.strings.ChatList_EmptyResult_SearchInAll, font: Font.regular(17.0), textColor: item.theme.list.itemAccentColor),
maximumNumberOfLines: 1,
truncationType: .end,
constrainedSize: CGSize(width: params.width - params.leftInset * 2.0 - 12.0 * 2.0, height: 1000.0)
))
var contentHeight: CGFloat = 0.0
let topInset: CGFloat = 40.0
let bottomInset: CGFloat = 10.0
let iconSpacing: CGFloat = 20.0
let titleSpacing: CGFloat = 6.0
let buttonSpacing: CGFloat = 14.0
let buttonInset: CGFloat = 11.0
let iconSize = CGSize(width: 128.0, height: 128.0)
contentHeight += topInset
contentHeight += iconSize.height
contentHeight += iconSpacing
contentHeight += titleLayout.0.size.height
contentHeight += titleSpacing
contentHeight += textLayout.0.size.height
if item.searchAllMessages != nil {
contentHeight += buttonSpacing
contentHeight += buttonInset
contentHeight += searchAllMessagesTitleLayout.0.size.height
contentHeight += buttonInset
}
contentHeight += bottomInset
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: UIEdgeInsets())
return (layout, { [weak self] in
guard let self else {
return
}
self.item = item
self.contentSize = layout.contentSize
self.insets = layout.insets
let _ = titleLayout.1()
let _ = textLayout.1()
let _ = searchAllMessagesTitleLayout.1()
var contentY: CGFloat = 0.0
contentY += topInset
let _ = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: "ChatListNoResults"
),
color: nil,
placeholderColor: nil,
startingPosition: .begin,
size: iconSize,
renderingScale: nil,
loop: false,
playOnce: nil
)),
environment: {}, containerSize: iconSize
)
let iconFrame = CGRect(origin: CGPoint(x: floor((params.width - iconSize.width) * 0.5), y: contentY), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.contentNode.view.addSubview(iconView)
}
iconView.frame = iconFrame
}
contentY += iconSize.height
contentY += iconSpacing
let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: contentY), size: titleLayout.0.size)
self.titleNode.frame = titleFrame
contentY += titleLayout.0.size.height
contentY += titleSpacing
let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: contentY), size: textLayout.0.size)
self.textNode.frame = textFrame
contentY += textLayout.0.size.height
if item.searchAllMessages != nil {
contentY += buttonSpacing
let searchAllMessagesButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - searchAllMessagesTitleLayout.0.size.width) * 0.5), y: contentY), size: CGSize(width: searchAllMessagesTitleLayout.0.size.width, height: searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0))
contentY += searchAllMessagesTitleLayout.0.size.height + buttonInset * 2.0
self.searchAllMessagesButton.frame = searchAllMessagesButtonFrame
self.searchAllMessagesTitle.frame = CGRect(origin: CGPoint(x: 0.0, y: buttonInset), size: searchAllMessagesTitleLayout.0.size)
contentY += buttonInset
contentY += searchAllMessagesTitleLayout.0.size.height
contentY += buttonInset
}
contentY += bottomInset
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: self.contentNode.frame.minY), size: CGSize(width: params.width, height: contentHeight))
self.contentNode.frame = contentFrame
})
}
}
}

View file

@ -28,6 +28,7 @@ import EmojiStatusComponent
import AvatarVideoNode
import AppBundle
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import ShimmerEffect
public enum ChatListItemContent {
@ -82,10 +83,10 @@ public enum ChatListItemContent {
public struct Tag: Equatable {
public var id: Int32
public var title: String
public var title: ChatFolderTitle
public var colorId: Int32
public init(id: Int32, title: String, colorId: Int32) {
public init(id: Int32, title: ChatFolderTitle, colorId: Int32) {
self.id = id
self.title = title
self.colorId = colorId
@ -269,6 +270,8 @@ private final class ChatListItemTagListComponent: Component {
let backgroundView: UIImageView
let title = ComponentView<Empty>()
private var currentTitle: ChatFolderTitle?
override init(frame: CGRect) {
self.backgroundView = UIImageView(image: tagBackgroundImage)
@ -281,11 +284,20 @@ private final class ChatListItemTagListComponent: Component {
preconditionFailure()
}
func update(context: AccountContext, title: String, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize {
func update(context: AccountContext, title: ChatFolderTitle, backgroundColor: UIColor, foregroundColor: UIColor, sizeFactor: CGFloat) -> CGSize {
self.currentTitle = title
let titleValue = ChatFolderTitle(text: title.text.isEmpty ? " " : title.text, entities: title.entities, enableAnimations: title.enableAnimations)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title.isEmpty ? " " : title, font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor))
component: AnyComponent(MultilineTextWithEntitiesComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
placeholderColor: foregroundColor.withMultipliedAlpha(0.1),
text: .plain(titleValue.attributedString(font: Font.semibold(floor(11.0 * sizeFactor)), textColor: foregroundColor)),
manualVisibilityControl: true,
resetAnimationsOnVisibilityChange: true
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
@ -309,11 +321,30 @@ private final class ChatListItemTagListComponent: Component {
return backgroundSize
}
func updateVisibility(_ isVisible: Bool) {
guard let currentTitle = self.currentTitle else {
return
}
if let titleView = self.title.view as? MultilineTextWithEntitiesComponent.View {
titleView.updateVisibility(isVisible && currentTitle.enableAnimations)
}
}
}
final class View: UIView {
private var itemViews: [Int32: ItemView] = [:]
var isVisible: Bool = false {
didSet {
if self.isVisible != oldValue {
for (_, itemView) in self.itemViews {
itemView.updateVisibility(self.isVisible)
}
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
@ -332,13 +363,13 @@ private final class ChatListItemTagListComponent: Component {
}
let itemId: Int32
let itemTitle: String
let itemTitle: ChatFolderTitle
let itemBackgroundColor: UIColor
let itemForegroundColor: UIColor
if validIds.count >= 3 {
itemId = Int32.max
itemTitle = "+\(component.tags.count - validIds.count)"
itemTitle = ChatFolderTitle(text: "+\(component.tags.count - validIds.count)", entities: [], enableAnimations: true)
itemForegroundColor = component.theme.chatList.dateTextColor
itemBackgroundColor = itemForegroundColor.withMultipliedAlpha(0.1)
} else {
@ -347,7 +378,7 @@ private final class ChatListItemTagListComponent: Component {
let tagColor = PeerNameColor(rawValue: tag.colorId)
let resolvedColor = component.context.peerNameColors.getChatFolderTag(tagColor, dark: component.theme.overallDarkAppearance)
itemTitle = tag.title.uppercased()
itemTitle = ChatFolderTitle(text: tag.title.text.uppercased(), entities: tag.title.entities, enableAnimations: tag.title.enableAnimations)
itemBackgroundColor = resolvedColor.main.withMultipliedAlpha(0.1)
itemForegroundColor = resolvedColor.main
}
@ -364,6 +395,7 @@ private final class ChatListItemTagListComponent: Component {
let itemSize = itemView.update(context: component.context, title: itemTitle, backgroundColor: itemBackgroundColor, foregroundColor: itemForegroundColor, sizeFactor: component.sizeFactor)
let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize)
itemView.frame = itemFrame
itemView.updateVisibility(self.isVisible)
validIds.append(itemId)
nextX += itemSize.width
@ -1451,6 +1483,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
)
}
self.authorNode.visibilityStatus = self.visibilityStatus
if let itemTagListView = self.itemTagList?.view as? ChatListItemTagListComponent.View {
itemTagListView.isVisible = self.visibilityStatus
}
}
}
}
@ -4239,13 +4275,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
environment: {},
containerSize: itemTagListFrame.size
)
if let itemTagListView = itemTagList.view {
if let itemTagListView = itemTagList.view as? ChatListItemTagListComponent.View {
if itemTagListView.superview == nil {
itemTagListView.isUserInteractionEnabled = false
strongSelf.mainContentContainerNode.view.addSubview(itemTagListView)
}
itemTagListTransition.updateFrame(view: itemTagListView, frame: itemTagListFrame)
itemTagListView.isVisible = strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji
}
} else {
if let itemTagList = strongSelf.itemTagList {

View file

@ -4276,33 +4276,32 @@ public final class ChatListNode: ListView {
}
}
private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (String, Bool, Bool, ContactsPeerItemStatus.Icon?)? {
private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (NSAttributedString, Bool, Bool, ContactsPeerItemStatus.Icon?)? {
if accountPeerId == peer.id {
return nil
}
if displayAutoremoveTimeout {
if let autoremoveTimeout = autoremoveTimeout {
return (strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string, false, true, .autoremove)
return (NSAttributedString(string: strings.ChatList_LabelAutodeleteAfter(timeIntervalString(strings: strings, value: autoremoveTimeout, usage: .afterTime)).string), false, true, .autoremove)
} else {
return (strings.ChatList_LabelAutodeleteDisabled, false, false, .autoremove)
return (NSAttributedString(string: strings.ChatList_LabelAutodeleteDisabled), false, false, .autoremove)
}
}
if let chatListFilters = chatListFilters {
var result = ""
let result = NSMutableAttributedString(string: "")
for case let .filter(_, title, _, data) in chatListFilters {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
if !result.isEmpty {
result.append(", ")
if result.length != 0 {
result.append(NSAttributedString(string: ", "))
}
//TODO:release
result.append(title.text)
result.append(title.rawAttributedString)
}
}
if result.isEmpty {
if result.length == 0 {
return nil
} else {
return (result, true, false, nil)
@ -4313,30 +4312,30 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
return nil
} else if case let .user(user) = peer {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return (strings.ChatList_PeerTypeBot, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeBot), false, false, nil)
} else {
if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeContact), false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContactUser, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil)
}
}
} else if case .secretChat = peer {
if isContact {
return (strings.ChatList_PeerTypeContact, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeContact), false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContactUser, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil)
}
} else if case .legacyGroup = peer {
return (strings.ChatList_PeerTypeGroup, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeGroup), false, false, nil)
} else if case let .channel(channel) = peer {
if case .group = channel.info {
return (strings.ChatList_PeerTypeGroup, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeGroup), false, false, nil)
} else {
return (strings.ChatList_PeerTypeChannel, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeChannel), false, false, nil)
}
}
return (strings.ChatList_PeerTypeNonContactUser, false, false, nil)
return (NSAttributedString(string: strings.ChatList_PeerTypeNonContactUser), false, false, nil)
}
public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
@ -4425,10 +4424,9 @@ func chatListItemTags(location: ChatListControllerLocation, accountPeerId: Engin
if data.color != nil {
let predicate = chatListFilterPredicate(filter: data, accountPeerId: accountPeerId)
if predicate.pinnedPeerIds.contains(peer.id) || predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
//TODO:release
result.append(ChatListItemContent.Tag(
id: id,
title: title.text,
title: title,
colorId: data.color?.rawValue ?? PeerNameColor.blue.rawValue
))
}

View file

@ -31,6 +31,8 @@ public final class MultilineTextWithEntitiesComponent: Component {
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let handleSpoilers: Bool
public let manualVisibilityControl: Bool
public let resetAnimationsOnVisibilityChange: Bool
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
@ -52,6 +54,8 @@ public final class MultilineTextWithEntitiesComponent: Component {
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
handleSpoilers: Bool = false,
manualVisibilityControl: Bool = false,
resetAnimationsOnVisibilityChange: Bool = false,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
@ -73,6 +77,8 @@ public final class MultilineTextWithEntitiesComponent: Component {
self.highlightColor = highlightColor
self.highlightAction = highlightAction
self.handleSpoilers = handleSpoilers
self.manualVisibilityControl = manualVisibilityControl
self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange
self.tapAction = tapAction
self.longTapAction = longTapAction
}
@ -105,6 +111,12 @@ public final class MultilineTextWithEntitiesComponent: Component {
if lhs.handleSpoilers != rhs.handleSpoilers {
return false
}
if lhs.manualVisibilityControl != rhs.manualVisibilityControl {
return false
}
if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
@ -151,6 +163,10 @@ public final class MultilineTextWithEntitiesComponent: Component {
fatalError("init(coder:) has not been implemented")
}
public func updateVisibility(_ isVisible: Bool) {
self.textNode.visibility = isVisible
}
public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let attributedString: NSAttributedString
switch component.text {
@ -176,6 +192,8 @@ public final class MultilineTextWithEntitiesComponent: Component {
self.textNode.highlightAttributeAction = component.highlightAction
self.textNode.tapAttributeAction = component.tapAction
self.textNode.longTapAttributeAction = component.longTapAction
self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange
if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string {
if let snapshotView = self.snapshotContentTree() {
@ -189,7 +207,9 @@ public final class MultilineTextWithEntitiesComponent: Component {
}
}
self.textNode.visibility = true
if !component.manualVisibilityControl {
self.textNode.visibility = true
}
if let context = component.context, let animationCache = component.animationCache, let animationRenderer = component.animationRenderer, let placeholderColor = component.placeholderColor {
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,

View file

@ -82,6 +82,7 @@ public final class ListComposePollOptionComponent: Component {
public let backspaceKeyAction: (() -> Void)?
public let selection: Selection?
public let inputMode: InputMode?
public let alwaysDisplayInputModeSelector: Bool
public let toggleInputMode: (() -> Void)?
public let tag: AnyObject?
@ -100,6 +101,7 @@ public final class ListComposePollOptionComponent: Component {
backspaceKeyAction: (() -> Void)?,
selection: Selection?,
inputMode: InputMode?,
alwaysDisplayInputModeSelector: Bool = false,
toggleInputMode: (() -> Void)?,
tag: AnyObject? = nil
) {
@ -117,6 +119,7 @@ public final class ListComposePollOptionComponent: Component {
self.backspaceKeyAction = backspaceKeyAction
self.selection = selection
self.inputMode = inputMode
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
self.toggleInputMode = toggleInputMode
self.tag = tag
}
@ -158,6 +161,9 @@ public final class ListComposePollOptionComponent: Component {
if lhs.inputMode != rhs.inputMode {
return false
}
if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector {
return false
}
return true
}
@ -490,15 +496,17 @@ public final class ListComposePollOptionComponent: Component {
ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001)
}
if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View {
animationView.playOnce()
if let animationView = modeSelectorView.contentView as? LottieComponent.View {
if playAnimation {
animationView.playOnce()
}
}
modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center)
modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size))
if let externalState = component.externalState {
let displaySelector = externalState.isEditing
let displaySelector = externalState.isEditing || component.alwaysDisplayInputModeSelector
alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0)
alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001)

View file

@ -165,19 +165,19 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
if let _ = peer as? TelegramUser {
status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat)
} else if let group = peer as? TelegramGroup {
status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil)
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if let participantCount = participantCount, participantCount != 0 {
status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(participantCount)), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Group_Status), multiline: false, isActive: false, icon: nil)
}
} else {
if let participantCount = participantCount, participantCount != 0 {
status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Conversation_StatusSubscribers(participantCount)), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Channel_Status), multiline: false, isActive: false, icon: nil)
}
}
} else {
@ -227,7 +227,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
let text: String
text = presentationData.strings.ChatList_ArchiveStoryCount(Int32(storyData.count))
status = .custom(string: text, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: text), multiline: false, isActive: false, icon: nil)
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in

View file

@ -674,7 +674,7 @@ public class ContactsController: ViewController {
let text = self.presentationData.strings.ContactList_DeletedContacts(Int32(peerIds.count))
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: text, text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(context: self.context, title: NSAttributedString(string: text), text: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
guard let self else {
return false
}

View file

@ -52,7 +52,7 @@ private enum InviteContactsEntry: Comparable, Identifiable {
case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder):
let status: ContactsPeerItemStatus
if count != 0 {
status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: strings.Contacts_ImportersCount(count)), multiline: false, isActive: false, icon: nil)
} else {
status = .none
}

View file

@ -32,6 +32,8 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/MoreButtonNode",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View file

@ -21,6 +21,8 @@ import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
import MoreButtonNode
import TextFormat
import TextNodeWithEntities
public final class ContactItemHighlighting {
public var chatLocation: ChatLocation?
@ -39,7 +41,7 @@ public enum ContactsPeerItemStatus {
case none
case presence(EnginePeer.Presence, PresentationDateTimeFormat)
case addressName(String)
case custom(string: String, multiline: Bool, isActive: Bool, icon: Icon?)
case custom(string: NSAttributedString, multiline: Bool, isActive: Bool, icon: Icon?)
}
public enum ContactsPeerItemSelection: Equatable {
@ -437,7 +439,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var credibilityIconComponent: EmojiStatusComponent?
private var verifiedIconView: ComponentHostView<Empty>?
private var verifiedIconComponent: EmojiStatusComponent?
public let statusNode: TextNode
public let statusNode: TextNodeWithEntities
private var statusIconNode: ASImageNode?
private var badgeBackgroundNode: ASImageNode?
private var badgeTextNode: TextNode?
@ -519,6 +521,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
containerSize: avatarIconView.bounds.size
)
}
self.statusNode.visibilityRect = self.visibilityStatus == false ? CGRect.zero : CGRect.infinite
}
}
}
@ -554,7 +557,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.avatarNode.isLayerBacked = false
self.titleNode = TextNode()
self.statusNode = TextNode()
self.statusNode = TextNodeWithEntities()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
@ -575,7 +578,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.avatarNodeContainer.addSubnode(self.avatarNode)
self.offsetContainerNode.addSubnode(self.avatarNodeContainer)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.statusNode)
self.offsetContainerNode.addSubnode(self.statusNode.textNode)
self.addSubnode(self.maskNode)
@ -708,7 +711,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let makeStatusLayout = TextNodeWithEntities.asyncLayout(self.statusNode)
let currentSelectionNode = self.selectionNode
let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
@ -939,7 +942,24 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .custom(text, multiline, isActive, icon):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
let statusAttributedStringValue = NSMutableAttributedString(string: text.string)
statusAttributedStringValue.addAttribute(.font, value: statusFont, range: NSRange(location: 0, length: statusAttributedStringValue.length))
statusAttributedStringValue.addAttribute(.foregroundColor, value: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor, range: NSRange(location: 0, length: statusAttributedStringValue.length))
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in
for (key, value) in attributes {
if key == ChatTextInputAttributes.bold {
statusAttributedStringValue.addAttribute(.font, value: Font.semibold(14.0), range: range)
} else if key == ChatTextInputAttributes.italic {
statusAttributedStringValue.addAttribute(.font, value: Font.italic(14.0), range: range)
} else if key == ChatTextInputAttributes.monospace {
statusAttributedStringValue.addAttribute(.font, value: Font.monospace(14.0), range: range)
} else {
statusAttributedStringValue.addAttribute(key, value: value, range: range)
}
}
})
statusAttributedString = statusAttributedStringValue
statusIcon = icon
statusIsActive = isActive
multilineStatus = multiline
@ -964,7 +984,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
switch item.status {
case let .custom(text, multiline, isActive, icon):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
let statusAttributedStringValue = NSMutableAttributedString(string: "")
statusAttributedStringValue.addAttribute(.font, value: statusFont, range: NSRange(location: 0, length: text.length))
statusAttributedStringValue.addAttribute(.foregroundColor, value: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor, range: NSRange(location: 0, length: text.length))
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), using: { attributes, range, _ in
for (key, value) in attributes {
if key == ChatTextInputAttributes.bold {
statusAttributedStringValue.addAttribute(.font, value: Font.semibold(14.0), range: range)
} else if key == ChatTextInputAttributes.italic {
statusAttributedStringValue.addAttribute(.font, value: Font.italic(14.0), range: range)
} else if key == ChatTextInputAttributes.monospace {
statusAttributedStringValue.addAttribute(.font, value: Font.monospace(14.0), range: range)
} else {
statusAttributedStringValue.addAttribute(key, value: value, range: range)
}
}
})
statusAttributedString = statusAttributedStringValue
multilineStatus = multiline
statusIsActive = isActive
statusIcon = icon
@ -1352,6 +1388,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let _ = titleApply()
var titleLeftOffset: CGFloat = 0.0
var nextIconX: CGFloat = titleFrame.maxX
if let verifiedIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
@ -1375,17 +1412,30 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
emojiFileUpdated: nil
)
strongSelf.verifiedIconComponent = verifiedIconComponent
let iconOrigin: CGFloat
if case .animation = verifiedIcon {
iconOrigin = titleFrame.minX
} else {
nextIconX += 4.0
iconOrigin = nextIconX
}
let containerSize = CGSize(width: 16.0, height: 16.0)
let iconSize = verifiedIconView.update(
transition: .immediate,
component: AnyComponent(verifiedIconComponent),
environment: {},
containerSize: CGSize(width: 16.0, height: 16.0)
containerSize: containerSize
)
transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
titleLeftOffset += iconSize.width + 4.0
if case .animation = verifiedIcon {
titleLeftOffset += iconSize.width + 4.0
} else {
nextIconX += iconSize.width
}
} else if let verifiedIconView = strongSelf.verifiedIconView {
strongSelf.verifiedIconView = nil
verifiedIconView.removeFromSuperview()
@ -1395,17 +1445,24 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.4
strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0
strongSelf.statusNode.textNode.alpha = item.enabled ? 1.0 : 1.0
let _ = statusApply()
strongSelf.statusNode.visibilityRect = strongSelf.visibilityStatus == false ? CGRect.zero : CGRect.infinite
let _ = statusApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor,
attemptSynchronous: false
))
var statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY - 1.0), size: statusLayout.size)
if let statusIconImage {
statusFrame.origin.x += statusIconImage.size.width + 1.0
}
let previousStatusFrame = strongSelf.statusNode.frame
let previousStatusFrame = strongSelf.statusNode.textNode.frame
strongSelf.statusNode.frame = statusFrame
transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
strongSelf.statusNode.textNode.frame = statusFrame
transition.animatePositionAdditive(node: strongSelf.statusNode.textNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
if let statusIconImage {
let statusIconNode: ASImageNode
@ -1413,7 +1470,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
statusIconNode = current
} else {
statusIconNode = ASImageNode()
strongSelf.statusNode.addSubnode(statusIconNode)
strongSelf.statusNode.textNode.addSubnode(statusIconNode)
}
statusIconNode.image = statusIconImage
statusIconNode.frame = CGRect(origin: CGPoint(x: -statusIconImage.size.width - 1.0, y: floor((statusFrame.height - statusIconImage.size.height) / 2.0) + 1.0), size: statusIconImage.size)
@ -1424,7 +1481,6 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
var nextIconX: CGFloat = titleFrame.maxX
if let credibilityIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer

View file

@ -317,7 +317,7 @@ public final class ContextControllerActionsListActionItemNode: HighlightTracking
if !self.item.entities.isEmpty {
let inputStateText = ChatTextInputStateText(text: self.item.text, attributes: self.item.entities.compactMap { entity -> ChatTextInputStateTextAttribute? in
if case let .CustomEmoji(_, fileId) = entity.type {
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId), range: entity.range)
return ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: fileId, enableAnimation: true), range: entity.range)
}
return nil
})

View file

@ -106,6 +106,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case disableCallV2(Bool)
case experimentalCallMute(Bool)
case conferenceCalls(Bool)
case playerV2(Bool)
case benchmarkReflectors
case enableLocalTranslation(Bool)
case preferredVideoCodec(Int, String, String?, Bool)
@ -132,7 +133,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .conferenceCalls, .benchmarkReflectors, .enableLocalTranslation:
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .disableCallV2, .experimentalCallMute, .conferenceCalls, .playerV2, .benchmarkReflectors, .enableLocalTranslation:
return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue
@ -251,12 +252,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 52
case .conferenceCalls:
return 53
case .benchmarkReflectors:
case .playerV2:
return 54
case .enableLocalTranslation:
case .benchmarkReflectors:
return 55
case .enableLocalTranslation:
return 56
case let .preferredVideoCodec(index, _, _, _):
return 56 + index
return 57 + index
case .disableVideoAspectScaling:
return 100
case .enableNetworkFramework:
@ -1355,6 +1358,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
})
}).start()
})
case let .playerV2(value):
return ItemListSwitchItem(presentationData: presentationData, title: "PlayerV2", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
settings.playerV2 = value
return PreferencesEntry(settings)
})
}).start()
})
case .benchmarkReflectors:
return ItemListActionItem(presentationData: presentationData, title: "Benchmark Reflectors", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
@ -1578,6 +1591,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute))
entries.append(.conferenceCalls(experimentalSettings.conferenceCalls))
entries.append(.playerV2(experimentalSettings.playerV2))
entries.append(.benchmarkReflectors)
entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))

View file

@ -126,7 +126,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
}
let attributedString = NSMutableAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center)
if let range = attributedString.string.range(of: "⭐️") {
if let range = attributedString.string.range(of: "$") {
attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.foregroundColor, value: color, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))

View file

@ -330,7 +330,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
private func setupWithVideo(_ file: TelegramMediaFile) {
let videoNode = UniversalVideoNode(
accountId: self.context.account.id,
context: self.context,
postbox: self.context.account.postbox,
audioSession: self.context.sharedContext.mediaManager.audioSession,
manager: self.context.sharedContext.mediaManager.universalVideoManager,

View file

@ -4,7 +4,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface FFMpegAVCodec : NSObject
+ (FFMpegAVCodec * _Nullable)findForId:(int)codecId;
+ (FFMpegAVCodec * _Nullable)findForId:(int)codecId preferHardwareAccelerationCapable:(bool)preferHardwareAccelerationCapable;
- (void *)impl;

View file

@ -25,6 +25,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVCodecContextReceiveResult)
- (bool)open;
- (bool)sendEnd;
- (void)setupHardwareAccelerationIfPossible;
- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame;
- (void)flushBuffers;

View file

@ -12,6 +12,11 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFramePixelFormat) {
FFMpegAVFramePixelFormatYUVA
};
typedef NS_ENUM(NSUInteger, FFMpegAVFrameNativePixelFormat) {
FFMpegAVFrameNativePixelFormatUnknown,
FFMpegAVFrameNativePixelFormatVideoToolbox
};
@interface FFMpegAVFrame : NSObject
@property (nonatomic, readonly) int32_t width;
@ -27,6 +32,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFramePixelFormat) {
- (instancetype)initWithPixelFormat:(FFMpegAVFramePixelFormat)pixelFormat width:(int32_t)width height:(int32_t)height;
- (void *)impl;
- (FFMpegAVFrameNativePixelFormat)nativePixelFormat;
@end

View file

@ -18,7 +18,26 @@
return self;
}
+ (FFMpegAVCodec * _Nullable)findForId:(int)codecId {
+ (FFMpegAVCodec * _Nullable)findForId:(int)codecId preferHardwareAccelerationCapable:(_Bool)preferHardwareAccelerationCapable {
if (preferHardwareAccelerationCapable && codecId == AV_CODEC_ID_AV1) {
void *codecIterationState = nil;
while (true) {
AVCodec const *codec = av_codec_iterate(&codecIterationState);
if (!codec) {
break;
}
if (!av_codec_is_decoder(codec)) {
continue;
}
if (codec->id != codecId) {
continue;
}
if (strncmp(codec->name, "av1", 2) == 0) {
return [[FFMpegAVCodec alloc] initWithImpl:codec];
}
}
}
AVCodec const *codec = avcodec_find_decoder(codecId);
if (codec) {
return [[FFMpegAVCodec alloc] initWithImpl:codec];

View file

@ -6,6 +6,10 @@
#import "libavformat/avformat.h"
#import "libavcodec/avcodec.h"
static enum AVPixelFormat getPreferredPixelFormat(__unused AVCodecContext *ctx, __unused const enum AVPixelFormat *pix_fmts) {
return AV_PIX_FMT_VIDEOTOOLBOX;
}
@interface FFMpegAVCodecContext () {
FFMpegAVCodec *_codec;
AVCodecContext *_impl;
@ -61,6 +65,11 @@
return status == 0;
}
- (void)setupHardwareAccelerationIfPossible {
av_hwdevice_ctx_create(&_impl->hw_device_ctx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, nil, nil, 0);
_impl->get_format = getPreferredPixelFormat;
}
- (FFMpegAVCodecContextReceiveResult)receiveIntoFrame:(FFMpegAVFrame *)frame {
int status = avcodec_receive_frame(_impl, (AVFrame *)[frame impl]);
if (status == 0) {

View file

@ -64,6 +64,17 @@
return _impl->pts;
}
- (FFMpegAVFrameNativePixelFormat)nativePixelFormat {
switch (_impl->format) {
case AV_PIX_FMT_VIDEOTOOLBOX: {
return FFMpegAVFrameNativePixelFormatVideoToolbox;
}
default: {
return FFMpegAVFrameNativePixelFormatUnknown;
}
}
}
- (int64_t)duration {
#if LIBAVFORMAT_VERSION_MAJOR >= 59
return _impl->duration;

View file

@ -259,7 +259,7 @@ public func galleryItemForEntry(
}
if isHLS {
content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos, codecConfiguration: HLSCodecConfiguration(context: context))
content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos, autoFetchFullSizeThumbnail: true, codecConfiguration: HLSCodecConfiguration(context: context))
} else {
content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file))
}

View file

@ -1762,7 +1762,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoScale: CGFloat
if item.content is WebEmbedVideoContent {
videoScale = 1.0
@ -2849,7 +2849,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: {
let overlayNode = OverlayUniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
@ -3073,7 +3073,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.disablePictureInPicturePlaceholder = true
let overlayVideoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay)
let overlayVideoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay)
let absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil)
overlayVideoNode.frame = absoluteRect
overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate)
@ -3156,7 +3156,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
shouldBeDismissed = .single(false)
}
let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: {
let overlayNode = OverlayUniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)

View file

@ -1449,7 +1449,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
}
self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
self.context.sharedContext.mediaManager.setPlaylist((self.context, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
return
}

View file

@ -58,7 +58,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
fileValue = file
}
self.videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
self.videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
self.videoNode.isUserInteractionEnabled = false
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))

View file

@ -62,7 +62,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case peer(EnginePeer.Id)
}
case header(String)
case header(NSAttributedString)
case mainLinkHeader(String)
case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool)
@ -225,13 +225,13 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
private func folderInviteLinkListControllerEntries(
presentationData: PresentationData,
state: FolderInviteLinkListControllerState,
title: String,
title: ChatFolderTitle,
allPeers: [EnginePeer]
) -> [InviteLinksListEntry] {
var entries: [InviteLinksListEntry] = []
var infoString: String?
let chatCountString: String
let chatCountString: NSAttributedString
let peersHeaderString: String
let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) })
@ -241,16 +241,36 @@ private func folderInviteLinkListControllerEntries(
if !canShareChats {
infoString = presentationData.strings.FolderLinkScreen_TitleDescriptionUnavailable
chatCountString = presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable
chatCountString = NSAttributedString(string: presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable)
peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderUnavailable
} else if state.selectedPeerIds.isEmpty {
chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionDeselected(title).string
let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionDeselectedV2)
let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
chatCountStringValue.replaceCharacters(in: folderRange, with: "")
chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location)
}
chatCountString = chatCountStringValue
peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeader
if allPeers.count > 1 {
selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll
}
} else {
chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionSelected(title, presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count))).string
let chatCountStringValue = NSMutableAttributedString(string: presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedV2)
let folderRange = (chatCountStringValue.string as NSString).range(of: "{folder}")
if folderRange.location != NSNotFound {
chatCountStringValue.replaceCharacters(in: folderRange, with: "")
chatCountStringValue.insert(title.rawAttributedString, at: folderRange.location)
}
let chatsRange = (chatCountStringValue.string as NSString).range(of: "{chats}")
if chatsRange.location != NSNotFound {
chatCountStringValue.replaceCharacters(in: chatsRange, with: "")
let countValue = presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count))
chatCountStringValue.insert(NSAttributedString(string: countValue), at: chatsRange.location)
}
chatCountString = chatCountStringValue
peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderSelected(Int32(state.selectedPeerIds.count))
if allPeers.count > 1 {
selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll
@ -699,11 +719,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
//TODO:release
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries(
presentationData: presentationData,
state: state,
title: filterTitle.text,
title: filterTitle,
allPeers: allPeers
), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges)

View file

@ -11,18 +11,19 @@ import TelegramAnimatedStickerNode
import AccountContext
import Markdown
import TextFormat
import TextNodeWithEntities
public class InviteLinkHeaderItem: ListViewItem, ItemListItem {
public let context: AccountContext
public let theme: PresentationTheme
public let title: String?
public let text: String
public let text: NSAttributedString
public let animationName: String
public let hideOnSmallScreens: Bool
public let sectionId: ItemListSectionId
public let linkAction: ((ItemListTextItemLinkAction) -> Void)?
public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: String, animationName: String, hideOnSmallScreens: Bool = false, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) {
public init(context: AccountContext, theme: PresentationTheme, title: String? = nil, text: NSAttributedString, animationName: String, hideOnSmallScreens: Bool = false, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) {
self.context = context
self.theme = theme
self.title = title
@ -75,7 +76,7 @@ private let textFont = Font.regular(14.0)
class InviteLinkHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private let textNode: TextNode
private let textNode: TextNodeWithEntities
private var animationNode: AnimatedStickerNode
private var item: InviteLinkHeaderItem?
@ -86,17 +87,17 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.contentMode = .left
self.textNode.textNode.contentsScale = UIScreen.main.scale
self.animationNode = DefaultAnimatedStickerNodeImpl()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.textNode.textNode)
self.addSubnode(self.animationNode)
}
@ -112,7 +113,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
return { item, params, neighbors in
let leftInset: CGFloat = 24.0 + params.leftInset
@ -131,9 +132,22 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
let attributedTitle = NSAttributedString(string: item.title ?? "", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}))
let attributedText = NSMutableAttributedString(string: item.text.string)
attributedText.addAttribute(.font, value: Font.regular(14.0), range: NSRange(location: 0, length: attributedText.length))
attributedText.addAttribute(.foregroundColor, value: item.theme.list.freeTextColor, range: NSRange(location: 0, length: attributedText.length))
item.text.enumerateAttributes(in: NSRange(location: 0, length: item.text.length), using: { attributes, range, _ in
for (key, value) in attributes {
if key == ChatTextInputAttributes.bold {
attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range)
} else if key == ChatTextInputAttributes.italic {
attributedText.addAttribute(.font, value: Font.italic(14.0), range: range)
} else if key == ChatTextInputAttributes.monospace {
attributedText.addAttribute(.font, value: Font.monospace(14.0), range: range)
} else {
attributedText.addAttribute(key, value: value, range: range)
}
}
})
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
@ -168,8 +182,15 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
origin += titleLayout.size.height + spacing
}
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size)
let _ = textApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.context.animationCache,
renderer: item.context.animationRenderer,
placeholderColor: item.theme.list.mediaPlaceholderColor,
attemptSynchronous: true
))
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: origin), size: textLayout.size)
strongSelf.textNode.visibilityRect = .infinite
}
})
}
@ -189,9 +210,9 @@ class InviteLinkHeaderItemNode: ListViewItemNode {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let textFrame = self.textNode.frame
let textFrame = self.textNode.textNode.frame
if let item = self.item, textFrame.contains(location) {
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) {
if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
item.linkAction?(.tap(url))
}

View file

@ -56,7 +56,7 @@ private enum InviteLinksListSection: Int32 {
}
private enum InviteLinksListEntry: ItemListNodeEntry {
case header(PresentationTheme, String)
case header(PresentationTheme, NSAttributedString)
case mainLinkHeader(PresentationTheme, String)
case mainLink(PresentationTheme, ExportedInvitation?, [EnginePeer], Int32, Bool)
@ -278,7 +278,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData,
} else {
helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelp
}
entries.append(.header(presentationData.theme, helpText))
entries.append(.header(presentationData.theme, NSAttributedString(string: helpText)))
}
let mainInvite: ExportedInvitation?

View file

@ -100,7 +100,7 @@ private enum InviteRequestsEntry: ItemListNodeEntry {
let arguments = arguments as! InviteRequestsControllerArguments
switch self {
case let .header(theme, text):
return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: text, animationName: "Requests", sectionId: self.section, linkAction: { _ in
return InviteLinkHeaderItem(context: arguments.context, theme: theme, text: NSAttributedString(string: text), animationName: "Requests", sectionId: self.section, linkAction: { _ in
arguments.openLinks()
})
case let .requestsHeader(_, text):

View file

@ -1436,6 +1436,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
var titleFrame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)
var titleLeftOffset: CGFloat = 0.0
var nextIconX: CGFloat = titleFrame.maxX
if let verifiedIcon = verifiedIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
@ -1461,6 +1462,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
emojiFileUpdated: nil
)
strongSelf.verifiedIconComponent = verifiedIconComponent
let iconOrigin: CGFloat
if case .animation = verifiedIcon {
iconOrigin = titleFrame.minX
} else {
nextIconX += 4.0
iconOrigin = nextIconX
}
let iconSize = verifiedIconView.update(
transition: .immediate,
component: AnyComponent(verifiedIconComponent),
@ -1468,9 +1478,13 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
containerSize: CGSize(width: 20.0, height: 20.0)
)
transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
titleLeftOffset += iconSize.width + 4.0
transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: iconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
if case .animation = verifiedIcon {
titleLeftOffset += iconSize.width + 4.0
} else {
nextIconX += iconSize.width
}
} else if let verifiedIconView = strongSelf.verifiedIconView {
strongSelf.verifiedIconView = nil
verifiedIconView.removeFromSuperview()
@ -1512,7 +1526,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
containerSize: CGSize(width: 20.0, height: 20.0)
)
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
nextIconX += 4.0
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize))
} else if let credibilityIconView = strongSelf.credibilityIconView {
strongSelf.credibilityIconView = nil
credibilityIconView.removeFromSuperview()

View file

@ -10,7 +10,7 @@ public func convertOpusToAAC(sourcePath: String, allocateTempFile: @escaping ()
queue.async {
do {
let audioSource = SoftwareAudioSource(path: sourcePath, focusedPart: nil)
let audioSource = SoftwareAudioSource(path: sourcePath)
let outputPath = allocateTempFile()

View file

@ -21,6 +21,7 @@ swift_library(
"//submodules/YuvConversion:YuvConversion",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/TextFormat:TextFormat",
"//submodules/ManagedFile",
],
visibility = [
"//visibility:public",

View file

@ -9,7 +9,7 @@ import TelegramAudio
public final class ChunkMediaPlayerPart {
public enum Id: Hashable {
case tempFile(path: String)
case directFile(path: String, audio: DirectStream?, video: DirectStream?)
case directStream
}
public struct DirectStream: Hashable {
@ -26,51 +26,29 @@ public final class ChunkMediaPlayerPart {
}
}
public enum Content {
public final class TempFile {
public let file: TempBoxFile
public init(file: TempBoxFile) {
self.file = file
}
deinit {
TempBox.shared.dispose(self.file)
}
public final class TempFile {
public let file: TempBoxFile
public init(file: TempBoxFile) {
self.file = file
}
public final class FFMpegDirectFile {
public let path: String
public let audio: DirectStream?
public let video: DirectStream?
public init(path: String, audio: DirectStream?, video: DirectStream?) {
self.path = path
self.audio = audio
self.video = video
}
deinit {
TempBox.shared.dispose(self.file)
}
case tempFile(TempFile)
case directFile(FFMpegDirectFile)
}
public let startTime: Double
public let endTime: Double
public let content: Content
public let content: TempFile
public let clippedStartTime: Double?
public let codecName: String?
public var id: Id {
switch self.content {
case let .tempFile(tempFile):
return .tempFile(path: tempFile.file.path)
case let .directFile(directFile):
return .directFile(path: directFile.path, audio: directFile.audio, video: directFile.video)
}
return .tempFile(path: self.content.file.path)
}
public init(startTime: Double, clippedStartTime: Double? = nil, endTime: Double, content: Content, codecName: String?) {
public init(startTime: Double, clippedStartTime: Double? = nil, endTime: Double, content: TempFile, codecName: String?) {
self.startTime = startTime
self.clippedStartTime = clippedStartTime
self.endTime = endTime
@ -80,12 +58,63 @@ public final class ChunkMediaPlayerPart {
}
public final class ChunkMediaPlayerPartsState {
public let duration: Double?
public let parts: [ChunkMediaPlayerPart]
public final class DirectReader {
public struct Stream {
public let mediaBox: MediaBox
public let resource: MediaResource
public let size: Int64
public let index: Int
public let seek: (streamIndex: Int, pts: Int64)
public let maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
public let codecName: String?
public init(mediaBox: MediaBox, resource: MediaResource, size: Int64, index: Int, seek: (streamIndex: Int, pts: Int64), maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?, codecName: String?) {
self.mediaBox = mediaBox
self.resource = resource
self.size = size
self.index = index
self.seek = seek
self.maxReadablePts = maxReadablePts
self.codecName = codecName
}
}
public final class Impl {
public let video: Stream?
public let audio: Stream?
public init(video: Stream?, audio: Stream?) {
self.video = video
self.audio = audio
}
}
public let id: Int
public let seekPosition: Double
public let availableUntilPosition: Double
public let bufferedUntilEnd: Bool
public let impl: Impl?
public init(id: Int, seekPosition: Double, availableUntilPosition: Double, bufferedUntilEnd: Bool, impl: Impl?) {
self.id = id
self.seekPosition = seekPosition
self.availableUntilPosition = availableUntilPosition
self.bufferedUntilEnd = bufferedUntilEnd
self.impl = impl
}
}
public init(duration: Double?, parts: [ChunkMediaPlayerPart]) {
public enum Content {
case parts([ChunkMediaPlayerPart])
case directReader(DirectReader)
}
public let duration: Double?
public let content: Content
public init(duration: Double?, content: Content) {
self.duration = duration
self.parts = parts
self.content = content
}
}

View file

@ -6,390 +6,454 @@ import TelegramCore
import FFMpegBinding
import RangeSet
private final class FFMpegMediaFrameExtractContext {
let fd: Int32
var readPosition: Int = 0
let size: Int
private func FFMpegLookaheadReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<FFMpegLookaheadReader>.fromOpaque(userData!).takeUnretainedValue()
var accessedRanges = RangeSet<Int>()
var maskRanges: RangeSet<Int>?
var recordAccessedRanges = false
let readCount = min(256 * 1024, Int64(bufferSize))
let requestRange: Range<Int64> = context.readingOffset ..< (context.readingOffset + readCount)
init(fd: Int32, size: Int) {
self.fd = fd
self.size = size
}
}
private func FFMpegMediaFrameExtractContextReadPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<FFMpegMediaFrameExtractContext>.fromOpaque(userData!).takeUnretainedValue()
if context.recordAccessedRanges {
context.accessedRanges.insert(contentsOf: context.readPosition ..< (context.readPosition + Int(bufferSize)))
var fetchedData: Data?
let fetchDisposable = MetaDisposable()
let semaphore = DispatchSemaphore(value: 0)
let disposable = context.params.getDataInRange(requestRange, { data in
if let data {
fetchedData = data
semaphore.signal()
}
})
var isCancelled = false
let cancelDisposable = context.params.cancel.start(next: { _ in
isCancelled = true
semaphore.signal()
})
semaphore.wait()
if isCancelled {
context.isCancelled = true
}
let result: Int
if let maskRanges = context.maskRanges {
let readRange = context.readPosition ..< (context.readPosition + Int(bufferSize))
let _ = maskRanges
let _ = readRange
result = read(context.fd, buffer, Int(bufferSize))
disposable.dispose()
cancelDisposable.dispose()
fetchDisposable.dispose()
if let fetchedData = fetchedData {
fetchedData.withUnsafeBytes { byteBuffer -> Void in
guard let bytes = byteBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memcpy(buffer, bytes, fetchedData.count)
}
let fetchedCount = Int32(fetchedData.count)
context.setReadingOffset(offset: context.readingOffset + Int64(fetchedCount))
if fetchedCount == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return fetchedCount
} else {
result = read(context.fd, buffer, Int(bufferSize))
}
context.readPosition += Int(bufferSize)
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
}
private func FFMpegMediaFrameExtractContextSeekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegMediaFrameExtractContext>.fromOpaque(userData!).takeUnretainedValue()
private func FFMpegLookaheadReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegLookaheadReader>.fromOpaque(userData!).takeUnretainedValue()
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
return Int64(context.size)
return context.params.size
} else {
context.readPosition = Int(offset)
lseek(context.fd, off_t(offset), SEEK_SET)
context.setReadingOffset(offset: offset)
return offset
}
}
private struct FFMpegFrameSegment {
struct Stream {
let index: Int
let startPts: CMTime
let startPosition: Int64
var endPts: CMTime
var endPosition: Int64
var duration: Double
}
var audio: Stream?
var video: Stream?
init() {
}
mutating func addFrame(isVideo: Bool, index: Int, pts: CMTime, duration: Double, position: Int64, size: Int64) {
if var stream = isVideo ? self.video : self.audio {
stream.endPts = pts
stream.duration += duration
stream.endPosition = max(stream.endPosition, position + size)
if isVideo {
self.video = stream
} else {
self.audio = stream
}
} else {
let stream = Stream(index: index, startPts: pts, startPosition: position, endPts: pts, endPosition: position + size, duration: duration)
if isVideo {
self.video = stream
} else {
self.audio = stream
}
}
}
private func range(_ outer: Range<Int64>, fullyContains inner: Range<Int64>) -> Bool {
return inner.lowerBound >= outer.lowerBound && inner.upperBound <= outer.upperBound
}
private final class FFMpegFrameSegmentInfo {
let headerAccessRanges: RangeSet<Int>
let segments: [FFMpegFrameSegment]
private final class FFMpegLookaheadReader {
let params: FFMpegLookaheadThread.Params
init(headerAccessRanges: RangeSet<Int>, segments: [FFMpegFrameSegment]) {
self.headerAccessRanges = headerAccessRanges
self.segments = segments
}
}
private func extractFFMpegFrameSegmentInfo(path: String) -> FFMpegFrameSegmentInfo? {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
var avIoContext: FFMpegAVIOContext?
var avFormatContext: FFMpegAVFormatContext?
var s = stat()
stat(path, &s)
let size = Int32(s.st_size)
var audioStream: FFMpegFileReader.StreamInfo?
var videoStream: FFMpegFileReader.StreamInfo?
let fd = open(path, O_RDONLY, S_IRUSR)
if fd < 0 {
return nil
}
defer {
close(fd)
}
var seekInfo: FFMpegLookaheadThread.State.Seek?
var maxReadPts: FFMpegLookaheadThread.State.Seek?
var audioStreamState: FFMpegLookaheadThread.StreamState?
var videoStreamState: FFMpegLookaheadThread.StreamState?
let avFormatContext = FFMpegAVFormatContext()
let ioBufferSize = 32 * 1024
var reportedState: FFMpegLookaheadThread.State?
let context = FFMpegMediaFrameExtractContext(fd: fd, size: Int(size))
context.recordAccessedRanges = true
var readingOffset: Int64 = 0
var isCancelled: Bool = false
var isEnded: Bool = false
guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(context).toOpaque(), readPacket: FFMpegMediaFrameExtractContextReadPacketCallback, writePacket: nil, seek: FFMpegMediaFrameExtractContextSeekCallback, isSeekable: true) else {
return nil
}
private var currentFetchRange: Range<Int64>?
private var currentFetchDisposable: Disposable?
avFormatContext.setIO(avIoContext)
var currentTimestamp: Double?
if !avFormatContext.openInput(withDirectFilePath: nil) {
return nil
}
if !avFormatContext.findStreamInfo() {
return nil
}
var audioStream: FFMpegMediaInfo.Info?
var videoStream: FFMpegMediaInfo.Info?
for typeIndex in 0 ..< 2 {
let isVideo = typeIndex == 0
init?(params: FFMpegLookaheadThread.Params) {
self.params = params
for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime))
var codecName: String?
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
if codecId == FFMpegCodecIdMPEG4 {
codecName = "mpeg4"
} else if codecId == FFMpegCodecIdH264 {
codecName = "h264"
} else if codecId == FFMpegCodecIdHEVC {
codecName = "hevc"
} else if codecId == FFMpegCodecIdAV1 {
codecName = "av1"
} else if codecId == FFMpegCodecIdVP9 {
codecName = "vp9"
} else if codecId == FFMpegCodecIdVP8 {
codecName = "vp8"
}
let info = FFMpegMediaInfo.Info(
index: Int(streamIndex),
timescale: timebase.timescale,
startTime: startTime,
duration: duration,
fps: fps,
codecName: codecName
)
if isVideo {
videoStream = info
} else {
audioStream = info
}
let ioBufferSize = 64 * 1024
guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: FFMpegLookaheadReader_readPacketCallback, writePacket: nil, seek: FFMpegLookaheadReader_seekCallback, isSeekable: true) else {
return nil
}
}
var segments: [FFMpegFrameSegment] = []
let maxSegmentDuration: Double = 5.0
if let videoStream {
let indexEntryCount = avFormatContext.numberOfIndexEntries(atStreamIndex: Int32(videoStream.index))
self.avIoContext = avIoContext
if indexEntryCount > 0 {
let frameDuration = 1.0 / videoStream.fps.seconds
var indexEntry = FFMpegAVIndexEntry()
for i in 0 ..< indexEntryCount {
if !avFormatContext.fillIndexEntry(atStreamIndex: Int32(videoStream.index), entryIndex: Int32(i), outEntry: &indexEntry) {
let avFormatContext = FFMpegAVFormatContext()
avFormatContext.setIO(avIoContext)
if !avFormatContext.openInput(withDirectFilePath: nil) {
return nil
}
if !avFormatContext.findStreamInfo() {
return nil
}
self.avFormatContext = avFormatContext
var audioStream: FFMpegFileReader.StreamInfo?
var videoStream: FFMpegFileReader.StreamInfo?
for streamType in 0 ..< 2 {
let isVideo = streamType == 0
for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) {
let streamIndex = streamIndexNumber.int32Value
if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) {
continue
}
let packetPts = CMTime(value: indexEntry.timestamp, timescale: videoStream.timescale)
//print("index: \(packetPts.seconds), isKeyframe: \(indexEntry.isKeyframe), position: \(indexEntry.pos), size: \(indexEntry.size)")
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
var startNewSegment = segments.isEmpty
if indexEntry.isKeyframe {
if segments.isEmpty {
startNewSegment = true
} else if let video = segments[segments.count - 1].video {
if packetPts.seconds - video.startPts.seconds > maxSegmentDuration {
startNewSegment = true
}
}
}
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
if startNewSegment {
segments.append(FFMpegFrameSegment())
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex)
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
segments[segments.count - 1].addFrame(isVideo: true, index: videoStream.index, pts: packetPts, duration: frameDuration, position: indexEntry.pos, size: Int64(indexEntry.size))
}
if !segments.isEmpty, let video = segments[segments.count - 1].video {
if video.endPts.seconds + 1.0 / videoStream.fps.seconds + 0.001 < videoStream.duration.seconds {
segments[segments.count - 1].video?.duration = videoStream.duration.seconds - video.startPts.seconds
segments[segments.count - 1].video?.endPts = videoStream.duration
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime))
//let metrics = avFormatContext.metricsForStream(at: streamIndex)
//let rotationAngle: Double = metrics.rotationAngle
//let aspect = Double(metrics.width) / Double(metrics.height)
let stream = FFMpegFileReader.StreamInfo(
index: streamIndexNumber.intValue,
codecId: codecId,
startTime: startTime,
duration: duration,
timeBase: timebase.value,
timeScale: timebase.timescale,
fps: fps
)
if isVideo {
videoStream = stream
} else {
audioStream = stream
}
}
}
self.audioStream = audioStream
self.videoStream = videoStream
if let preferredStream = self.videoStream ?? self.audioStream {
let pts = CMTimeMakeWithSeconds(params.seekToTimestamp, preferredTimescale: preferredStream.timeScale)
self.seekInfo = FFMpegLookaheadThread.State.Seek(streamIndex: preferredStream.index, pts: pts.value)
avFormatContext.seekFrame(forStreamIndex: Int32(preferredStream.index), pts: pts.value, positionOnKeyframe: true)
}
self.updateCurrentTimestamp()
}
if let audioStream {
let indexEntryCount = avFormatContext.numberOfIndexEntries(atStreamIndex: Int32(audioStream.index))
if indexEntryCount > 0 {
var minSegmentIndex = 0
var minSegmentStartTime: Double = -100000.0
let frameDuration = 1.0 / audioStream.fps.seconds
var indexEntry = FFMpegAVIndexEntry()
for i in 0 ..< indexEntryCount {
if !avFormatContext.fillIndexEntry(atStreamIndex: Int32(audioStream.index), entryIndex: Int32(i), outEntry: &indexEntry) {
continue
}
let packetPts = CMTime(value: indexEntry.timestamp, timescale: audioStream.timescale)
//print("index: \(packetPts.value), timestamp: \(packetPts.seconds), isKeyframe: \(indexEntry.isKeyframe), position: \(indexEntry.pos), size: \(indexEntry.size)")
if videoStream != nil {
for i in minSegmentIndex ..< segments.count {
if let video = segments[i].video {
if minSegmentStartTime <= packetPts.seconds && video.endPts.seconds >= packetPts.seconds {
segments[i].addFrame(isVideo: false, index: audioStream.index, pts: packetPts, duration: frameDuration, position: indexEntry.pos, size: Int64(indexEntry.size))
if minSegmentIndex != i {
minSegmentIndex = i
minSegmentStartTime = video.startPts.seconds
}
break
}
}
deinit {
self.currentFetchDisposable?.dispose()
}
func setReadingOffset(offset: Int64) {
self.readingOffset = offset
let readRange: Range<Int64> = offset ..< (offset + 512 * 1024)
if !self.params.isDataCachedInRange(readRange) {
if let currentFetchRange = self.currentFetchRange {
if currentFetchRange.overlaps(readRange) {
if !range(currentFetchRange, fullyContains: readRange) {
self.setFetchRange(range: currentFetchRange.lowerBound ..< max(currentFetchRange.upperBound, readRange.upperBound + 2 * 1024 * 1024))
}
} else {
if segments.isEmpty {
segments.append(FFMpegFrameSegment())
}
segments[segments.count - 1].addFrame(isVideo: false, index: audioStream.index, pts: packetPts, duration: frameDuration, position: indexEntry.pos, size: Int64(indexEntry.size))
self.setFetchRange(range: offset ..< (offset + 2 * 1024 * 1024))
}
}
}
if !segments.isEmpty, let audio = segments[segments.count - 1].audio {
if audio.endPts.seconds + 0.001 < audioStream.duration.seconds {
segments[segments.count - 1].audio?.duration = audioStream.duration.seconds - audio.startPts.seconds
segments[segments.count - 1].audio?.endPts = audioStream.duration
} else {
self.setFetchRange(range: offset ..< (offset + 2 * 1024 * 1024))
}
}
}
let headerAccessRanges = context.accessedRanges
private func setFetchRange(range: Range<Int64>) {
if self.currentFetchRange != range {
self.currentFetchRange = range
self.currentFetchDisposable?.dispose()
self.currentFetchDisposable = self.params.fetchInRange(range)
}
}
for i in 1 ..< segments.count {
let segment = segments[i]
func updateCurrentTimestamp() {
self.currentTimestamp = self.params.currentTimestamp.with({ $0 })
if let video = segment.video {
context.maskRanges = headerAccessRanges
context.maskRanges?.insert(contentsOf: Int(video.startPosition) ..< Int(video.endPosition))
self.updateReadIfNeeded()
}
private func updateReadIfNeeded() {
guard let avFormatContext = self.avFormatContext else {
return
}
guard let currentTimestamp = self.currentTimestamp else {
return
}
let maxPtsSeconds = max(self.params.seekToTimestamp, currentTimestamp) + 10.0
var currentAudioPtsSecondsAdvanced: Double = 0.0
var currentVideoPtsSecondsAdvanced: Double = 0.0
let packet = FFMpegPacket()
while !self.isCancelled && !self.isEnded {
var audioAlreadyRead: Bool = false
var videoAlreadyRead: Bool = false
context.accessedRanges = RangeSet()
context.recordAccessedRanges = true
avFormatContext.seekFrame(forStreamIndex: Int32(video.index), byteOffset: video.startPosition)
let packet = FFMpegPacket()
while true {
if !avFormatContext.readFrame(into: packet) {
break
}
if Int(packet.streamIndex) == video.index {
let packetPts = CMTime(value: packet.pts, timescale: video.startPts.timescale)
if packetPts.value >= video.endPts.value {
break
}
if let audioStreamState = self.audioStreamState {
if audioStreamState.readableToTime.seconds >= maxPtsSeconds {
audioAlreadyRead = true
}
} else if self.audioStream == nil {
audioAlreadyRead = true
}
print("Segment \(i): \(video.startPosition) ..< \(video.endPosition) accessed \(context.accessedRanges.ranges)")
if let videoStreamState = self.videoStreamState {
if videoStreamState.readableToTime.seconds >= maxPtsSeconds {
videoAlreadyRead = true
}
} else if self.videoStream == nil {
videoAlreadyRead = true
}
if audioAlreadyRead && videoAlreadyRead {
break
}
if !avFormatContext.readFrame(into: packet) {
self.isEnded = true
break
}
self.maxReadPts = FFMpegLookaheadThread.State.Seek(streamIndex: Int(packet.streamIndex), pts: packet.pts)
if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index {
let pts = CMTimeMake(value: packet.pts, timescale: audioStream.timeScale)
if let audioStreamState = self.audioStreamState {
currentAudioPtsSecondsAdvanced += pts.seconds - audioStreamState.readableToTime.seconds
}
self.audioStreamState = FFMpegLookaheadThread.StreamState(
info: audioStream,
readableToTime: pts
)
} else if let videoStream = self.videoStream, Int(packet.streamIndex) == videoStream.index {
let pts = CMTimeMake(value: packet.pts, timescale: videoStream.timeScale)
if let videoStreamState = self.videoStreamState {
currentVideoPtsSecondsAdvanced += pts.seconds - videoStreamState.readableToTime.seconds
}
self.videoStreamState = FFMpegLookaheadThread.StreamState(
info: videoStream,
readableToTime: pts
)
}
if min(currentAudioPtsSecondsAdvanced, currentVideoPtsSecondsAdvanced) >= 0.1 {
self.reportStateIfNeeded()
}
}
self.reportStateIfNeeded()
}
private func reportStateIfNeeded() {
guard let seekInfo = self.seekInfo else {
return
}
var stateIsFullyInitialised = true
if self.audioStream != nil && self.audioStreamState == nil {
stateIsFullyInitialised = false
}
if self.videoStream != nil && self.videoStreamState == nil {
stateIsFullyInitialised = false
}
let state = FFMpegLookaheadThread.State(
seek: seekInfo,
maxReadablePts: self.maxReadPts,
audio: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.audioStreamState : nil,
video: (stateIsFullyInitialised && self.maxReadPts != nil) ? self.videoStreamState : nil,
isEnded: self.isEnded
)
if self.reportedState != state {
self.reportedState = state
self.params.updateState(state)
}
}
}
private final class FFMpegLookaheadThread: NSObject {
struct StreamState: Equatable {
let info: FFMpegFileReader.StreamInfo
let readableToTime: CMTime
init(info: FFMpegFileReader.StreamInfo, readableToTime: CMTime) {
self.info = info
self.readableToTime = readableToTime
}
}
/*{
if let videoStream {
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
struct State: Equatable {
struct Seek: Equatable {
var streamIndex: Int
var pts: Int64
let packet = FFMpegPacket()
while true {
if !avFormatContext.readFrame(into: packet) {
break
}
if Int(packet.streamIndex) == videoStream.index {
let packetPts = CMTime(value: packet.pts, timescale: videoStream.timescale)
let packetDuration = CMTime(value: packet.duration, timescale: videoStream.timescale)
var startNewSegment = segments.isEmpty
if packet.isKeyframe {
if segments.isEmpty {
startNewSegment = true
} else if let video = segments[segments.count - 1].video {
if packetPts.seconds - video.startPts.seconds > maxSegmentDuration {
startNewSegment = true
}
}
}
if startNewSegment {
segments.append(FFMpegFrameSegment())
}
segments[segments.count - 1].addFrame(isVideo: true, index: Int(packet.streamIndex), pts: packetPts, duration: packetDuration.seconds)
}
init(streamIndex: Int, pts: Int64) {
self.streamIndex = streamIndex
self.pts = pts
}
}
if let audioStream {
avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: true)
var minSegmentIndex = 0
let packet = FFMpegPacket()
while true {
if !avFormatContext.readFrame(into: packet) {
break
}
if Int(packet.streamIndex) == audioStream.index {
let packetPts = CMTime(value: packet.pts, timescale: audioStream.timescale)
let packetDuration = CMTime(value: packet.duration, timescale: audioStream.timescale)
if videoStream != nil {
for i in minSegmentIndex ..< segments.count {
if let video = segments[i].video {
if video.startPts.seconds <= packetPts.seconds && video.endPts.seconds >= packetPts.seconds {
segments[i].addFrame(isVideo: false, index: Int(audioStream.index), pts: packetPts, duration: packetDuration.seconds)
minSegmentIndex = i
break
}
}
}
} else {
if segments.isEmpty {
segments.append(FFMpegFrameSegment())
}
segments[segments.count - 1].addFrame(isVideo: false, index: Int(packet.streamIndex), pts: packetPts, duration: packetDuration.seconds)
}
}
let seek: Seek
let maxReadablePts: Seek?
let audio: StreamState?
let video: StreamState?
let isEnded: Bool
init(seek: Seek, maxReadablePts: Seek?, audio: StreamState?, video: StreamState?, isEnded: Bool) {
self.seek = seek
self.maxReadablePts = maxReadablePts
self.audio = audio
self.video = video
self.isEnded = isEnded
}
}
final class Params: NSObject {
let seekToTimestamp: Double
let updateState: (State) -> Void
let fetchInRange: (Range<Int64>) -> Disposable
let getDataInRange: (Range<Int64>, @escaping (Data?) -> Void) -> Disposable
let isDataCachedInRange: (Range<Int64>) -> Bool
let size: Int64
let cancel: Signal<Void, NoError>
let currentTimestamp: Atomic<Double?>
init(
seekToTimestamp: Double,
updateState: @escaping (State) -> Void,
fetchInRange: @escaping (Range<Int64>) -> Disposable,
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
size: Int64,
cancel: Signal<Void, NoError>,
currentTimestamp: Atomic<Double?>
) {
self.seekToTimestamp = seekToTimestamp
self.updateState = updateState
self.fetchInRange = fetchInRange
self.getDataInRange = getDataInRange
self.isDataCachedInRange = isDataCachedInRange
self.size = size
self.cancel = cancel
self.currentTimestamp = currentTimestamp
}
}
@objc static func entryPoint(_ params: Params) {
let runLoop = RunLoop.current
let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: FFMpegLookaheadThread.self, selector: #selector(FFMpegLookaheadThread.none), userInfo: nil, repeats: false)
runLoop.add(timer, forMode: .common)
Thread.current.threadDictionary["FFMpegLookaheadThread_reader"] = FFMpegLookaheadReader(params: params)
while true {
runLoop.run(mode: .default, before: .distantFuture)
if Thread.current.threadDictionary["FFMpegLookaheadThread_stop"] != nil {
break
}
}
}*/
Thread.current.threadDictionary.removeObject(forKey: "FFMpegLookaheadThread_params")
}
/*for i in 0 ..< segments.count {
print("Segment \(i):\n video \(segments[i].video?.startPts.seconds ?? -1.0) ... \(segments[i].video?.endPts.seconds ?? -1.0)\n audio \(segments[i].audio?.startPts.seconds ?? -1.0) ... \(segments[i].audio?.endPts.seconds ?? -1.0)")
}*/
@objc static func none() {
}
return FFMpegFrameSegmentInfo(
headerAccessRanges: context.accessedRanges,
segments: segments
)
@objc static func stop() {
Thread.current.threadDictionary["FFMpegLookaheadThread_stop"] = "true"
}
@objc static func updateCurrentTimestamp() {
if let reader = Thread.current.threadDictionary["FFMpegLookaheadThread_reader"] as? FFMpegLookaheadReader {
reader.updateCurrentTimestamp()
}
}
}
private final class FFMpegLookahead {
private let cancel = Promise<Void>()
private let currentTimestamp = Atomic<Double?>(value: nil)
private let thread: Thread
init(
seekToTimestamp: Double,
updateState: @escaping (FFMpegLookaheadThread.State) -> Void,
fetchInRange: @escaping (Range<Int64>) -> Disposable,
getDataInRange: @escaping (Range<Int64>, @escaping (Data?) -> Void) -> Disposable,
isDataCachedInRange: @escaping (Range<Int64>) -> Bool,
size: Int64
) {
self.thread = Thread(
target: FFMpegLookaheadThread.self,
selector: #selector(FFMpegLookaheadThread.entryPoint(_:)),
object: FFMpegLookaheadThread.Params(
seekToTimestamp: seekToTimestamp,
updateState: updateState,
fetchInRange: fetchInRange,
getDataInRange: getDataInRange,
isDataCachedInRange: isDataCachedInRange,
size: size,
cancel: self.cancel.get(),
currentTimestamp: self.currentTimestamp
)
)
self.thread.name = "FFMpegLookahead"
self.thread.start()
}
deinit {
self.cancel.set(.single(Void()))
FFMpegLookaheadThread.self.perform(#selector(FFMpegLookaheadThread.stop), on: self.thread, with: nil, waitUntilDone: false)
}
func updateCurrentTimestamp(timestamp: Double) {
let _ = self.currentTimestamp.swap(timestamp)
FFMpegLookaheadThread.self.perform(#selector(FFMpegLookaheadThread.updateCurrentTimestamp), on: self.thread, with: timestamp as NSNumber, waitUntilDone: false)
}
}
final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl {
@ -401,7 +465,10 @@ final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl {
}
private var completeFetchDisposable: Disposable?
private var dataDisposable: Disposable?
private var seekTimestamp: Double?
private var currentLookaheadId: Int = 0
private var lookahead: FFMpegLookahead?
init(resource: ChunkMediaPlayerV2.SourceDescription.ResourceDescription) {
self.resource = resource
@ -416,71 +483,134 @@ final class ChunkMediaPlayerDirectFetchSourceImpl: ChunkMediaPlayerSourceImpl {
preferBackgroundReferenceRevalidation: true
).startStrict()
}
self.dataDisposable = (resource.postbox.mediaBox.resourceData(resource.reference.resource)
|> deliverOnMainQueue).startStrict(next: { [weak self] data in
guard let self else {
return
}
if data.complete {
if let mediaInfo = extractFFMpegMediaInfo(path: data.path), let mainTrack = mediaInfo.audio ?? mediaInfo.video, let segmentInfo = extractFFMpegFrameSegmentInfo(path: data.path) {
var parts: [ChunkMediaPlayerPart] = []
for segment in segmentInfo.segments {
guard let mainStream = segment.video ?? segment.audio else {
assertionFailure()
continue
}
parts.append(ChunkMediaPlayerPart(
startTime: mainStream.startPts.seconds,
endTime: mainStream.startPts.seconds + mainStream.duration,
content: .directFile(ChunkMediaPlayerPart.Content.FFMpegDirectFile(
path: data.path,
audio: segment.audio.flatMap { stream in
return ChunkMediaPlayerPart.DirectStream(
index: stream.index,
startPts: stream.startPts,
endPts: stream.endPts,
duration: stream.duration
)
},
video: segment.video.flatMap { stream in
return ChunkMediaPlayerPart.DirectStream(
index: stream.index,
startPts: stream.startPts,
endPts: stream.endPts,
duration: stream.duration
)
}
)),
codecName: mediaInfo.video?.codecName
))
}
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: mainTrack.duration.seconds,
parts: parts
)))
} else {
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: nil,
parts: []
)))
}
} else {
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: nil,
parts: []
)))
}
})
}
deinit {
self.completeFetchDisposable?.dispose()
self.dataDisposable?.dispose()
}
func updatePlaybackState(position: Double, isPlaying: Bool) {
func seek(id: Int, position: Double) {
self.seekTimestamp = position
self.currentLookaheadId += 1
let lookaheadId = self.currentLookaheadId
let resource = self.resource
let updateState: (FFMpegLookaheadThread.State) -> Void = { [weak self] state in
Queue.mainQueue().async {
guard let self else {
return
}
if self.currentLookaheadId != lookaheadId {
return
}
guard let mainTrack = state.video ?? state.audio else {
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: nil,
content: .directReader(ChunkMediaPlayerPartsState.DirectReader(
id: id,
seekPosition: position,
availableUntilPosition: position,
bufferedUntilEnd: true,
impl: nil
))
)))
return
}
var minAvailableUntilPosition: Double?
if let audio = state.audio {
if let minAvailableUntilPositionValue = minAvailableUntilPosition {
minAvailableUntilPosition = min(minAvailableUntilPositionValue, audio.readableToTime.seconds)
} else {
minAvailableUntilPosition = audio.readableToTime.seconds
}
}
if let video = state.video {
if let minAvailableUntilPositionValue = minAvailableUntilPosition {
minAvailableUntilPosition = min(minAvailableUntilPositionValue, video.readableToTime.seconds)
} else {
minAvailableUntilPosition = video.readableToTime.seconds
}
}
self.partsStateValue.set(.single(ChunkMediaPlayerPartsState(
duration: mainTrack.info.duration.seconds,
content: .directReader(ChunkMediaPlayerPartsState.DirectReader(
id: id,
seekPosition: position,
availableUntilPosition: minAvailableUntilPosition ?? position,
bufferedUntilEnd: state.isEnded,
impl: ChunkMediaPlayerPartsState.DirectReader.Impl(
video: state.video.flatMap { media -> ChunkMediaPlayerPartsState.DirectReader.Stream? in
guard let maxReadablePts = state.maxReadablePts else {
return nil
}
return ChunkMediaPlayerPartsState.DirectReader.Stream(
mediaBox: resource.postbox.mediaBox,
resource: resource.reference.resource,
size: resource.size,
index: media.info.index,
seek: (streamIndex: state.seek.streamIndex, pts: state.seek.pts),
maxReadablePts: (streamIndex: maxReadablePts.streamIndex, pts: maxReadablePts.pts, isEnded: state.isEnded),
codecName: resolveFFMpegCodecName(id: media.info.codecId)
)
},
audio: state.audio.flatMap { media -> ChunkMediaPlayerPartsState.DirectReader.Stream? in
guard let maxReadablePts = state.maxReadablePts else {
return nil
}
return ChunkMediaPlayerPartsState.DirectReader.Stream(
mediaBox: resource.postbox.mediaBox,
resource: resource.reference.resource,
size: resource.size,
index: media.info.index,
seek: (streamIndex: state.seek.streamIndex, pts: state.seek.pts),
maxReadablePts: (streamIndex: maxReadablePts.streamIndex, pts: maxReadablePts.pts, isEnded: state.isEnded),
codecName: resolveFFMpegCodecName(id: media.info.codecId)
)
}
)
))
)))
}
}
self.lookahead = FFMpegLookahead(
seekToTimestamp: position,
updateState: updateState,
fetchInRange: { range in
return fetchedMediaResource(
mediaBox: resource.postbox.mediaBox,
userLocation: resource.userLocation,
userContentType: resource.userContentType,
reference: resource.reference,
range: (range, .elevated),
statsCategory: resource.statsCategory,
preferBackgroundReferenceRevalidation: true
).startStrict()
},
getDataInRange: { range, completion in
return resource.postbox.mediaBox.resourceData(resource.reference.resource, size: resource.size, in: range, mode: .complete).start(next: { result, isComplete in
completion(isComplete ? result : nil)
})
},
isDataCachedInRange: { range in
return resource.postbox.mediaBox.internal_resourceDataIsCached(
id: resource.reference.resource.id,
size: resource.size,
in: range
)
},
size: self.resource.size
)
}
func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool) {
if self.seekTimestamp == seekTimestamp {
self.lookahead?.updateCurrentTimestamp(timestamp: position)
}
}
}

View file

@ -14,7 +14,8 @@ public let internal_isHardwareAv1Supported: Bool = {
protocol ChunkMediaPlayerSourceImpl: AnyObject {
var partsState: Signal<ChunkMediaPlayerPartsState, NoError> { get }
func updatePlaybackState(position: Double, isPlaying: Bool)
func seek(id: Int, position: Double)
func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool)
}
private final class ChunkMediaPlayerExternalSourceImpl: ChunkMediaPlayerSourceImpl {
@ -24,7 +25,10 @@ private final class ChunkMediaPlayerExternalSourceImpl: ChunkMediaPlayerSourceIm
self.partsState = partsState
}
func updatePlaybackState(position: Double, isPlaying: Bool) {
func seek(id: Int, position: Double) {
}
func updatePlaybackState(seekTimestamp: Double, position: Double, isPlaying: Bool) {
}
}
@ -32,14 +36,16 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
public enum SourceDescription {
public final class ResourceDescription {
public let postbox: Postbox
public let size: Int64
public let reference: MediaResourceReference
public let userLocation: MediaResourceUserLocation
public let userContentType: MediaResourceUserContentType
public let statsCategory: MediaResourceStatsCategory
public let fetchAutomatically: Bool
public init(postbox: Postbox, reference: MediaResourceReference, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, statsCategory: MediaResourceStatsCategory, fetchAutomatically: Bool) {
public init(postbox: Postbox, size: Int64, reference: MediaResourceReference, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, statsCategory: MediaResourceStatsCategory, fetchAutomatically: Bool) {
self.postbox = postbox
self.size = size
self.reference = reference
self.userLocation = userLocation
self.userContentType = userContentType
@ -52,10 +58,23 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
case directFetch(ResourceDescription)
}
public struct MediaDataReaderParams {
public var useV2Reader: Bool
public init(useV2Reader: Bool) {
self.useV2Reader = useV2Reader
}
}
private final class LoadedPart {
enum Content {
case tempFile(ChunkMediaPlayerPart.TempFile)
case directStream(ChunkMediaPlayerPartsState.DirectReader.Stream)
}
final class Media {
let queue: Queue
let content: ChunkMediaPlayerPart.Content
let content: Content
let mediaType: AVMediaType
let codecName: String?
@ -64,7 +83,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
var didBeginReading: Bool = false
var isFinished: Bool = false
init(queue: Queue, content: ChunkMediaPlayerPart.Content, mediaType: AVMediaType, codecName: String?) {
init(queue: Queue, content: Content, mediaType: AVMediaType, codecName: String?) {
assert(queue.isCurrent())
self.queue = queue
@ -77,12 +96,21 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
assert(self.queue.isCurrent())
}
func load() {
func load(params: MediaDataReaderParams) {
let reader: MediaDataReader
if case let .tempFile(tempFile) = self.content, self.mediaType == .video, (self.codecName == "av1" || self.codecName == "av01"), internal_isHardwareAv1Supported {
reader = AVAssetVideoDataReader(filePath: tempFile.file.path, isVideo: self.mediaType == .video)
} else {
reader = FFMpegMediaDataReader(content: self.content, isVideo: self.mediaType == .video, codecName: self.codecName)
switch self.content {
case let .tempFile(tempFile):
if self.mediaType == .video, (self.codecName == "av1" || self.codecName == "av01"), internal_isHardwareAv1Supported {
reader = AVAssetVideoDataReader(filePath: tempFile.file.path, isVideo: self.mediaType == .video)
} else {
if params.useV2Reader {
reader = FFMpegMediaDataReaderV2(content: .tempFile(tempFile), isVideo: self.mediaType == .video, codecName: self.codecName)
} else {
reader = FFMpegMediaDataReaderV1(filePath: tempFile.file.path, isVideo: self.mediaType == .video, codecName: self.codecName)
}
}
case let .directStream(directStream):
reader = FFMpegMediaDataReaderV2(content: .directStream(directStream), isVideo: self.mediaType == .video, codecName: self.codecName)
}
if self.mediaType == .video {
if reader.hasVideo {
@ -94,15 +122,23 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
}
}
}
func update(content: Content) {
if let reader = self.reader {
if let reader = reader as? FFMpegMediaDataReaderV2, case let .directStream(directStream) = content {
reader.update(content: .directStream(directStream))
} else {
assertionFailure()
}
}
}
}
final class MediaData {
let part: ChunkMediaPlayerPart
let video: Media?
let audio: Media?
init(part: ChunkMediaPlayerPart, video: Media?, audio: Media?) {
self.part = part
init(video: Media?, audio: Media?) {
self.video = video
self.audio = audio
}
@ -118,6 +154,8 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private final class LoadedPartsMediaData {
var ids: [ChunkMediaPlayerPart.Id] = []
var parts: [ChunkMediaPlayerPart.Id: LoadedPart.MediaData] = [:]
var directMediaData: LoadedPart.MediaData?
var directReaderId: Double?
var notifiedHasSound: Bool = false
var seekFromMinTimestamp: Double?
}
@ -125,6 +163,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private static let sharedDataQueue = Queue(name: "ChunkMediaPlayerV2-DataQueue")
private let dataQueue: Queue
private let mediaDataReaderParams: MediaDataReaderParams
private let audioSessionManager: ManagedAudioSession
private let onSeeked: (() -> Void)?
@ -132,7 +171,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private var videoRenderer: AVSampleBufferDisplayLayer
private var audioRenderer: AVSampleBufferAudioRenderer?
private var partsState = ChunkMediaPlayerPartsState(duration: nil, parts: [])
private var partsState = ChunkMediaPlayerPartsState(duration: nil, content: .parts([]))
private var loadedParts: [LoadedPart] = []
private var loadedPartsMediaData: QueueLocalObject<LoadedPartsMediaData>
private var hasSound: Bool = false
@ -155,12 +194,14 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
public var actionAtEnd: MediaPlayerActionAtEnd = .stop
private var didSeekOnce: Bool = false
private var isPlaying: Bool = false
private var baseRate: Double = 1.0
private var isSoundEnabled: Bool
private var isMuted: Bool
private var seekId: Int = 0
private var seekTimestamp: Double = 0.0
private var pendingSeekTimestamp: Double?
private var pendingContinuePlaybackAfterSeekToTimestamp: Double?
private var shouldNotifySeeked: Bool = false
@ -171,6 +212,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private var audioIsRequestingMediaData: Bool = false
private let source: ChunkMediaPlayerSourceImpl
private var didSetSourceSeek: Bool = false
private var partsStateDisposable: Disposable?
private var updateTimer: Foundation.Timer?
@ -178,6 +220,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private var hasAudioSession: Bool = false
public init(
params: MediaDataReaderParams,
audioSessionManager: ManagedAudioSession,
source: SourceDescription,
video: Bool,
@ -196,6 +239,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
) {
self.dataQueue = ChunkMediaPlayerV2.sharedDataQueue
self.mediaDataReaderParams = params
self.audioSessionManager = audioSessionManager
self.onSeeked = onSeeked
@ -328,6 +372,12 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
}
}
if !self.didSeekOnce {
self.didSeekOnce = true
self.seek(timestamp: 0.0, play: nil)
return
}
let timestamp: CMTime
if let pendingSeekTimestamp = self.pendingSeekTimestamp {
timestamp = CMTimeMakeWithSeconds(pendingSeekTimestamp, preferredTimescale: 44000)
@ -337,6 +387,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
let timestampSeconds = timestamp.seconds
self.source.updatePlaybackState(
seekTimestamp: self.seekTimestamp,
position: timestampSeconds,
isPlaying: self.isPlaying
)
@ -346,38 +397,16 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
duration = partsStateDuration
}
var validParts: [ChunkMediaPlayerPart] = []
let isBuffering: Bool
var minStartTime: Double = 0.0
for i in 0 ..< self.partsState.parts.count {
let part = self.partsState.parts[i]
let partStartTime = max(minStartTime, part.startTime)
let partEndTime = max(partStartTime, part.endTime)
if partStartTime >= partEndTime {
continue
}
var partMatches = false
if timestampSeconds >= partStartTime - 0.5 && timestampSeconds < partEndTime + 0.5 {
partMatches = true
}
if partMatches {
validParts.append(ChunkMediaPlayerPart(
startTime: part.startTime,
clippedStartTime: partStartTime == part.startTime ? nil : partStartTime,
endTime: part.endTime,
content: part.content,
codecName: part.codecName
))
minStartTime = max(minStartTime, partEndTime)
}
}
let mediaDataReaderParams = self.mediaDataReaderParams
if let lastValidPart = validParts.last {
for i in 0 ..< self.partsState.parts.count {
let part = self.partsState.parts[i]
switch self.partsState.content {
case let .parts(partsStateParts):
var validParts: [ChunkMediaPlayerPart] = []
var minStartTime: Double = 0.0
for i in 0 ..< partsStateParts.count {
let part = partsStateParts[i]
let partStartTime = max(minStartTime, part.startTime)
let partEndTime = max(partStartTime, part.endTime)
@ -385,7 +414,12 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
continue
}
if lastValidPart !== part && partStartTime > (lastValidPart.clippedStartTime ?? lastValidPart.startTime) && partStartTime <= lastValidPart.endTime + 0.5 {
var partMatches = false
if timestampSeconds >= partStartTime - 0.5 && timestampSeconds < partEndTime + 0.5 {
partMatches = true
}
if partMatches {
validParts.append(ChunkMediaPlayerPart(
startTime: part.startTime,
clippedStartTime: partStartTime == part.startTime ? nil : partStartTime,
@ -394,141 +428,247 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
codecName: part.codecName
))
minStartTime = max(minStartTime, partEndTime)
break
}
}
}
if validParts.isEmpty, let pendingContinuePlaybackAfterSeekToTimestamp = self.pendingContinuePlaybackAfterSeekToTimestamp {
for part in self.partsState.parts {
if pendingContinuePlaybackAfterSeekToTimestamp >= part.startTime - 0.2 && pendingContinuePlaybackAfterSeekToTimestamp < part.endTime {
self.renderSynchronizer.setRate(Float(self.renderSynchronizerRate), time: CMTimeMakeWithSeconds(part.startTime, preferredTimescale: 44000))
break
}
}
}
self.loadedParts.removeAll(where: { partState in
if !validParts.contains(where: { $0.id == partState.part.id }) {
return true
}
return false
})
for part in validParts {
if !self.loadedParts.contains(where: { $0.part.id == part.id }) {
self.loadedParts.append(LoadedPart(part: part))
self.loadedParts.sort(by: { $0.part.startTime < $1.part.startTime })
}
}
if self.pendingSeekTimestamp != nil {
return
}
let loadedParts = self.loadedParts
let dataQueue = self.dataQueue
let isSoundEnabled = self.isSoundEnabled
self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in
loadedPartsMediaData.ids = loadedParts.map(\.part.id)
for part in loadedParts {
if let loadedPart = loadedPartsMediaData.parts[part.part.id] {
if let audio = loadedPart.audio, audio.didBeginReading, !isSoundEnabled {
let cleanAudio = LoadedPart.Media(
if let lastValidPart = validParts.last {
for i in 0 ..< partsStateParts.count {
let part = partsStateParts[i]
let partStartTime = max(minStartTime, part.startTime)
let partEndTime = max(partStartTime, part.endTime)
if partStartTime >= partEndTime {
continue
}
if lastValidPart !== part && partStartTime > (lastValidPart.clippedStartTime ?? lastValidPart.startTime) && partStartTime <= lastValidPart.endTime + 0.5 {
validParts.append(ChunkMediaPlayerPart(
startTime: part.startTime,
clippedStartTime: partStartTime == part.startTime ? nil : partStartTime,
endTime: part.endTime,
content: part.content,
codecName: part.codecName
))
minStartTime = max(minStartTime, partEndTime)
break
}
}
}
if validParts.isEmpty, let pendingContinuePlaybackAfterSeekToTimestamp = self.pendingContinuePlaybackAfterSeekToTimestamp {
for part in partsStateParts {
if pendingContinuePlaybackAfterSeekToTimestamp >= part.startTime - 0.2 && pendingContinuePlaybackAfterSeekToTimestamp < part.endTime {
self.renderSynchronizer.setRate(Float(self.renderSynchronizerRate), time: CMTimeMakeWithSeconds(part.startTime, preferredTimescale: 44000))
break
}
}
}
self.loadedParts.removeAll(where: { partState in
if !validParts.contains(where: { $0.id == partState.part.id }) {
return true
}
return false
})
for part in validParts {
if !self.loadedParts.contains(where: { $0.part.id == part.id }) {
self.loadedParts.append(LoadedPart(part: part))
self.loadedParts.sort(by: { $0.part.startTime < $1.part.startTime })
}
}
var playableDuration: Double = 0.0
var previousValidPartEndTime: Double?
for part in partsStateParts {
if let previousValidPartEndTime {
if part.startTime > previousValidPartEndTime + 0.5 {
break
}
} else if !validParts.contains(where: { $0.id == part.id }) {
continue
}
let partDuration: Double
if part.startTime - 0.5 <= timestampSeconds && part.endTime + 0.5 > timestampSeconds {
partDuration = part.endTime - timestampSeconds
} else if part.startTime - 0.5 > timestampSeconds {
partDuration = part.endTime - part.startTime
} else {
partDuration = 0.0
}
playableDuration += partDuration
previousValidPartEndTime = part.endTime
}
if self.pendingSeekTimestamp != nil {
return
}
let loadedParts = self.loadedParts
let dataQueue = self.dataQueue
let isSoundEnabled = self.isSoundEnabled
self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in
loadedPartsMediaData.ids = loadedParts.map(\.part.id)
for part in loadedParts {
if let loadedPart = loadedPartsMediaData.parts[part.part.id] {
if let audio = loadedPart.audio, audio.didBeginReading, !isSoundEnabled {
let cleanAudio = LoadedPart.Media(
queue: dataQueue,
content: .tempFile(part.part.content),
mediaType: .audio,
codecName: part.part.codecName
)
cleanAudio.load(params: mediaDataReaderParams)
loadedPartsMediaData.parts[part.part.id] = LoadedPart.MediaData(
video: loadedPart.video,
audio: cleanAudio.reader != nil ? cleanAudio : nil
)
}
} else {
let video = LoadedPart.Media(
queue: dataQueue,
content: part.part.content,
content: .tempFile(part.part.content),
mediaType: .video,
codecName: part.part.codecName
)
video.load(params: mediaDataReaderParams)
let audio = LoadedPart.Media(
queue: dataQueue,
content: .tempFile(part.part.content),
mediaType: .audio,
codecName: part.part.codecName
)
cleanAudio.load()
audio.load(params: mediaDataReaderParams)
loadedPartsMediaData.parts[part.part.id] = LoadedPart.MediaData(
part: part.part,
video: loadedPart.video,
audio: cleanAudio.reader != nil ? cleanAudio : nil
video: video,
audio: audio.reader != nil ? audio : nil
)
}
} else {
let video = LoadedPart.Media(
queue: dataQueue,
content: part.part.content,
mediaType: .video,
codecName: part.part.codecName
)
video.load()
let audio = LoadedPart.Media(
queue: dataQueue,
content: part.part.content,
mediaType: .audio,
codecName: part.part.codecName
)
audio.load()
loadedPartsMediaData.parts[part.part.id] = LoadedPart.MediaData(
part: part.part,
video: video,
audio: audio.reader != nil ? audio : nil
)
}
}
var removedKeys: [ChunkMediaPlayerPart.Id] = []
for (id, _) in loadedPartsMediaData.parts {
if !loadedPartsMediaData.ids.contains(id) {
removedKeys.append(id)
}
}
for id in removedKeys {
loadedPartsMediaData.parts.removeValue(forKey: id)
}
if !loadedPartsMediaData.notifiedHasSound, let part = loadedPartsMediaData.parts.values.first {
loadedPartsMediaData.notifiedHasSound = true
let hasSound = part.audio?.reader != nil
Queue.mainQueue().async {
guard let self else {
return
var removedKeys: [ChunkMediaPlayerPart.Id] = []
for (id, _) in loadedPartsMediaData.parts {
if !loadedPartsMediaData.ids.contains(id) {
removedKeys.append(id)
}
if self.hasSound != hasSound {
self.hasSound = hasSound
self.updateInternalState()
}
for id in removedKeys {
loadedPartsMediaData.parts.removeValue(forKey: id)
}
if !loadedPartsMediaData.notifiedHasSound, let part = loadedPartsMediaData.parts.values.first {
loadedPartsMediaData.notifiedHasSound = true
let hasSound = part.audio?.reader != nil
Queue.mainQueue().async {
guard let self else {
return
}
if self.hasSound != hasSound {
self.hasSound = hasSound
self.updateInternalState()
}
}
}
}
}
var playableDuration: Double = 0.0
var previousValidPartEndTime: Double?
for part in self.partsState.parts {
if let previousValidPartEndTime {
if part.startTime > previousValidPartEndTime + 0.5 {
break
}
} else if !validParts.contains(where: { $0.id == part.id }) {
continue
}
let partDuration: Double
if part.startTime - 0.5 <= timestampSeconds && part.endTime + 0.5 > timestampSeconds {
partDuration = part.endTime - timestampSeconds
} else if part.startTime - 0.5 > timestampSeconds {
partDuration = part.endTime - part.startTime
if let previousValidPartEndTime, previousValidPartEndTime >= duration - 0.5 {
isBuffering = false
} else {
partDuration = 0.0
isBuffering = playableDuration < 1.0
}
case let .directReader(directReader):
var readerImpl: ChunkMediaPlayerPartsState.DirectReader.Impl?
var playableDuration: Double = 0.0
let directReaderSeekPosition = directReader.seekPosition
if directReader.id == self.seekId {
readerImpl = directReader.impl
playableDuration = max(0.0, directReader.availableUntilPosition - timestampSeconds)
if directReader.bufferedUntilEnd {
isBuffering = false
} else {
isBuffering = playableDuration < 1.0
}
} else {
playableDuration = 0.0
isBuffering = true
}
let dataQueue = self.dataQueue
self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in
if !loadedPartsMediaData.ids.isEmpty {
loadedPartsMediaData.ids = []
}
if !loadedPartsMediaData.parts.isEmpty {
loadedPartsMediaData.parts.removeAll()
}
if let readerImpl {
if let currentDirectMediaData = loadedPartsMediaData.directMediaData, let currentDirectReaderId = loadedPartsMediaData.directReaderId, currentDirectReaderId == directReaderSeekPosition {
if let video = currentDirectMediaData.video, let videoStream = readerImpl.video {
video.update(content: .directStream(videoStream))
}
if let audio = currentDirectMediaData.audio, let audioStream = readerImpl.audio {
audio.update(content: .directStream(audioStream))
}
} else {
let video = readerImpl.video.flatMap { media in
return LoadedPart.Media(
queue: dataQueue,
content: .directStream(media),
mediaType: .video,
codecName: media.codecName
)
}
video?.load(params: mediaDataReaderParams)
let audio = readerImpl.audio.flatMap { media in
return LoadedPart.Media(
queue: dataQueue,
content: .directStream(media),
mediaType: .audio,
codecName: media.codecName
)
}
audio?.load(params: mediaDataReaderParams)
loadedPartsMediaData.directMediaData = LoadedPart.MediaData(
video: video,
audio: audio
)
}
loadedPartsMediaData.directReaderId = directReaderSeekPosition
if !loadedPartsMediaData.notifiedHasSound {
loadedPartsMediaData.notifiedHasSound = true
let hasSound = readerImpl.audio != nil
Queue.mainQueue().async {
guard let self else {
return
}
if self.hasSound != hasSound {
self.hasSound = hasSound
self.updateInternalState()
}
}
}
} else {
loadedPartsMediaData.directMediaData = nil
loadedPartsMediaData.directReaderId = nil
}
}
if self.pendingSeekTimestamp != nil {
return
}
playableDuration += partDuration
previousValidPartEndTime = part.endTime
}
var effectiveRate: Double = 0.0
let isBuffering: Bool
if let previousValidPartEndTime, previousValidPartEndTime >= duration - 0.5 {
isBuffering = false
} else {
isBuffering = playableDuration < 1.0
}
if self.isPlaying {
if !isBuffering {
effectiveRate = self.baseRate
@ -671,7 +811,30 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
}
private func seek(timestamp: Double, play: Bool?, notify: Bool) {
let currentTimestamp: CMTime
if let pendingSeekTimestamp = self.pendingSeekTimestamp {
currentTimestamp = CMTimeMakeWithSeconds(pendingSeekTimestamp, preferredTimescale: 44000)
} else {
currentTimestamp = self.renderSynchronizer.currentTime()
}
let currentTimestampSeconds = currentTimestamp.seconds
if currentTimestampSeconds == timestamp {
if let play {
self.isPlaying = play
}
if notify {
self.shouldNotifySeeked = true
}
if !self.didSetSourceSeek {
self.didSetSourceSeek = true
self.source.seek(id: self.seekId, position: timestamp)
}
self.updateInternalState()
return
}
self.seekId += 1
self.seekTimestamp = timestamp
let seekId = self.seekId
self.pendingSeekTimestamp = timestamp
self.pendingContinuePlaybackAfterSeekToTimestamp = timestamp
@ -699,6 +862,9 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
audioRenderer.stopRequestingMediaData()
}
self.didSetSourceSeek = true
self.source.seek(id: self.seekId, position: timestamp)
self.loadedPartsMediaData.with { [weak self] loadedPartsMediaData in
loadedPartsMediaData.parts.removeAll()
loadedPartsMediaData.seekFromMinTimestamp = timestamp
@ -733,7 +899,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private func triggerRequestMediaData() {
let loadedPartsMediaData = self.loadedPartsMediaData
if !self.videoIsRequestingMediaData && "".isEmpty {
if !self.videoIsRequestingMediaData {
self.videoIsRequestingMediaData = true
let videoTarget: AVQueuedSampleBufferRendering
@ -804,7 +970,9 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
continue
}
media.didBeginReading = true
if var sampleBuffer = reader.readSampleBuffer() {
switch reader.readSampleBuffer() {
case let .frame(sampleBuffer):
var sampleBuffer = sampleBuffer
if let seekFromMinTimestamp = loadedPartsMediaData.seekFromMinTimestamp, CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds < seekFromMinTimestamp {
if isVideo {
var updatedSampleBuffer: CMSampleBuffer?
@ -829,7 +997,54 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
bufferTarget.enqueue(sampleBuffer)
hasData = true
continue outer
} else {
case .waitingForMoreData, .endOfStream, .error:
media.isFinished = true
}
}
outerDirect: while true {
guard let directMediaData = loadedPartsMediaData.directMediaData else {
break outer
}
guard let media = isVideo ? directMediaData.video : directMediaData.audio else {
break outer
}
if media.isFinished {
break outer
}
guard let reader = media.reader else {
break outer
}
media.didBeginReading = true
switch reader.readSampleBuffer() {
case let .frame(sampleBuffer):
var sampleBuffer = sampleBuffer
if let seekFromMinTimestamp = loadedPartsMediaData.seekFromMinTimestamp, CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds < seekFromMinTimestamp {
if isVideo {
var updatedSampleBuffer: CMSampleBuffer?
CMSampleBufferCreateCopy(allocator: nil, sampleBuffer: sampleBuffer, sampleBufferOut: &updatedSampleBuffer)
if let updatedSampleBuffer {
if let attachments = CMSampleBufferGetSampleAttachmentsArray(updatedSampleBuffer, createIfNecessary: true) {
let attachments = attachments as NSArray
let dict = attachments[0] as! NSMutableDictionary
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String)
sampleBuffer = updatedSampleBuffer
}
}
} else {
continue outer
}
}
/*if !isVideo {
print("Enqueue audio \(CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value) next: \(CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value + 1024)")
}*/
bufferTarget.enqueue(sampleBuffer)
hasData = true
continue outer
case .waitingForMoreData:
break outer
case .endOfStream, .error:
media.isFinished = true
}
}

View file

@ -0,0 +1,473 @@
import Foundation
#if !os(macOS)
import UIKit
#else
import AppKit
import TGUIKit
#endif
import CoreMedia
import SwiftSignalKit
import FFMpegBinding
import Postbox
import ManagedFile
private func FFMpegFileReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
guard let buffer else {
return FFMPEG_CONSTANT_AVERROR_EOF
}
let context = Unmanaged<FFMpegFileReader>.fromOpaque(userData!).takeUnretainedValue()
switch context.source {
case let .file(file):
let result = file.read(buffer, Int(bufferSize))
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
return Int32(result)
case let .resource(resource):
let readCount = min(256 * 1024, Int64(bufferSize))
let requestRange: Range<Int64> = resource.readingPosition ..< (resource.readingPosition + readCount)
//TODO:improve thread safe read if incomplete
if let (file, readSize) = resource.mediaBox.internal_resourceData(id: resource.resource.id, size: resource.size, in: requestRange) {
let result = file.read(buffer, readSize)
if result == 0 {
return FFMPEG_CONSTANT_AVERROR_EOF
}
resource.readingPosition += Int64(result)
return Int32(result)
} else {
return FFMPEG_CONSTANT_AVERROR_EOF
}
}
}
private func FFMpegFileReader_seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
let context = Unmanaged<FFMpegFileReader>.fromOpaque(userData!).takeUnretainedValue()
if (whence & FFMPEG_AVSEEK_SIZE) != 0 {
switch context.source {
case let .file(file):
return file.getSize() ?? 0
case let .resource(resource):
return resource.size
}
} else {
switch context.source {
case let .file(file):
let _ = file.seek(position: offset)
case let .resource(resource):
resource.readingPosition = offset
}
return offset
}
}
final class FFMpegFileReader {
enum SourceDescription {
case file(String)
case resource(mediaBox: MediaBox, resource: MediaResource, size: Int64)
}
final class StreamInfo: Equatable {
let index: Int
let codecId: Int32
let startTime: CMTime
let duration: CMTime
let timeBase: CMTimeValue
let timeScale: CMTimeScale
let fps: CMTime
init(index: Int, codecId: Int32, startTime: CMTime, duration: CMTime, timeBase: CMTimeValue, timeScale: CMTimeScale, fps: CMTime) {
self.index = index
self.codecId = codecId
self.startTime = startTime
self.duration = duration
self.timeBase = timeBase
self.timeScale = timeScale
self.fps = fps
}
static func ==(lhs: StreamInfo, rhs: StreamInfo) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.codecId != rhs.codecId {
return false
}
if lhs.startTime != rhs.startTime {
return false
}
if lhs.duration != rhs.duration {
return false
}
if lhs.timeBase != rhs.timeBase {
return false
}
if lhs.timeScale != rhs.timeScale {
return false
}
if lhs.fps != rhs.fps {
return false
}
return true
}
}
fileprivate enum Source {
final class Resource {
let mediaBox: MediaBox
let resource: MediaResource
let size: Int64
var readingPosition: Int64 = 0
init(mediaBox: MediaBox, resource: MediaResource, size: Int64) {
self.mediaBox = mediaBox
self.resource = resource
self.size = size
}
}
case file(ManagedFile)
case resource(Resource)
}
private enum Decoder {
case videoPassthrough(FFMpegMediaPassthroughVideoFrameDecoder)
case video(FFMpegMediaVideoFrameDecoder)
case audio(FFMpegAudioFrameDecoder)
func send(frame: MediaTrackDecodableFrame) -> Bool {
switch self {
case let .videoPassthrough(decoder):
decoder.send(frame: frame)
case let .video(decoder):
decoder.send(frame: frame)
case let .audio(decoder):
decoder.send(frame: frame)
}
}
func sendEnd() -> Bool {
switch self {
case let .videoPassthrough(decoder):
return decoder.sendEndToDecoder()
case let .video(decoder):
return decoder.sendEndToDecoder()
case let .audio(decoder):
return decoder.sendEndToDecoder()
}
}
}
private final class Stream {
let info: StreamInfo
let decoder: Decoder
init(info: StreamInfo, decoder: Decoder) {
self.info = info
self.decoder = decoder
}
}
enum SelectedStream {
enum MediaType {
case audio
case video
}
case mediaType(MediaType)
case index(Int)
}
enum ReadFrameResult {
case frame(MediaTrackFrame)
case waitingForMoreData
case endOfStream
case error
}
private(set) var readingError = false
private var stream: Stream?
private var avIoContext: FFMpegAVIOContext?
private var avFormatContext: FFMpegAVFormatContext?
fileprivate let source: Source
private var didSendEndToDecoder: Bool = false
private var hasReadToEnd: Bool = false
private var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
private var lastReadPts: (streamIndex: Int, pts: Int64)?
private var isWaitingForMoreData: Bool = false
public init?(source: SourceDescription, passthroughDecoder: Bool = false, useHardwareAcceleration: Bool, selectedStream: SelectedStream, seek: (streamIndex: Int, pts: Int64)?, maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
switch source {
case let .file(path):
guard let file = ManagedFile(queue: nil, path: path, mode: .read) else {
return nil
}
self.source = .file(file)
case let .resource(mediaBox, resource, size):
self.source = .resource(Source.Resource(mediaBox: mediaBox, resource: resource, size: size))
}
self.maxReadablePts = maxReadablePts
let avFormatContext = FFMpegAVFormatContext()
/*if hintVP9 {
avFormatContext.forceVideoCodecId(FFMpegCodecIdVP9)
}*/
let ioBufferSize = 64 * 1024
let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: FFMpegFileReader_readPacketCallback, writePacket: nil, seek: FFMpegFileReader_seekCallback, isSeekable: true)
self.avIoContext = avIoContext
avFormatContext.setIO(self.avIoContext!)
if !avFormatContext.openInput(withDirectFilePath: nil) {
self.readingError = true
return nil
}
if !avFormatContext.findStreamInfo() {
self.readingError = true
return nil
}
self.avFormatContext = avFormatContext
var stream: Stream?
outer: for mediaType in [.audio, .video] as [SelectedStream.MediaType] {
streamSearch: for streamIndexNumber in avFormatContext.streamIndices(for: mediaType == .video ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) {
let streamIndex = Int(streamIndexNumber.int32Value)
if avFormatContext.isAttachedPic(atStreamIndex: Int32(streamIndex)) {
continue
}
switch selectedStream {
case let .mediaType(selectedMediaType):
if mediaType != selectedMediaType {
continue streamSearch
}
case let .index(index):
if streamIndex != index {
continue streamSearch
}
}
let codecId = avFormatContext.codecId(atStreamIndex: Int32(streamIndex))
let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: Int32(streamIndex), defaultTimeBase: CMTimeMake(value: 1, timescale: 40000))
let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase)
let startTime: CMTime
let rawStartTime = avFormatContext.startTime(atStreamIndex: Int32(streamIndex))
if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) {
startTime = CMTime(value: 0, timescale: timebase.timescale)
} else {
startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale)
}
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: Int32(streamIndex)), timescale: timebase.timescale)
let metrics = avFormatContext.metricsForStream(at: Int32(streamIndex))
let rotationAngle: Double = metrics.rotationAngle
//let aspect = Double(metrics.width) / Double(metrics.height)
let info = StreamInfo(
index: streamIndex,
codecId: codecId,
startTime: startTime,
duration: duration,
timeBase: timebase.value,
timeScale: timebase.timescale,
fps: fps
)
switch mediaType {
case .video:
if passthroughDecoder {
var videoFormatData: FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData?
if codecId == FFMpegCodecIdMPEG4 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_MPEG4Video, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdH264 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_H264, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdHEVC {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_HEVC, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
} else if codecId == FFMpegCodecIdAV1 {
videoFormatData = FFMpegMediaPassthroughVideoFrameDecoder.VideoFormatData(codecType: kCMVideoCodecType_AV1, width: metrics.width, height: metrics.height, extraData: Data(bytes: metrics.extradata, count: Int(metrics.extradataSize)))
}
if let videoFormatData {
stream = Stream(
info: info,
decoder: .videoPassthrough(FFMpegMediaPassthroughVideoFrameDecoder(videoFormatData: videoFormatData, rotationAngle: rotationAngle))
)
break outer
}
} else {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: useHardwareAcceleration) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: Int32(streamIndex), to: codecContext) {
if useHardwareAcceleration {
codecContext.setupHardwareAccelerationIfPossible()
}
if codecContext.open() {
stream = Stream(
info: info,
decoder: .video(FFMpegMediaVideoFrameDecoder(codecContext: codecContext))
)
break outer
}
}
}
}
case .audio:
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: Int32(streamIndex), to: codecContext) {
if codecContext.open() {
stream = Stream(
info: info,
decoder: .audio(FFMpegAudioFrameDecoder(codecContext: codecContext, sampleRate: 48000, channelCount: 1))
)
break outer
}
}
}
}
}
}
guard let stream else {
self.readingError = true
return nil
}
self.stream = stream
if let seek {
avFormatContext.seekFrame(forStreamIndex: Int32(seek.streamIndex), pts: seek.pts, positionOnKeyframe: true)
} else {
avFormatContext.seekFrame(forStreamIndex: Int32(stream.info.index), pts: 0, positionOnKeyframe: true)
}
}
deinit {
}
private func readPacketInternal() -> FFMpegPacket? {
guard let avFormatContext = self.avFormatContext else {
return nil
}
if let maxReadablePts = self.maxReadablePts, !maxReadablePts.isEnded, let lastReadPts = self.lastReadPts, lastReadPts.streamIndex == maxReadablePts.streamIndex, lastReadPts.pts == maxReadablePts.pts {
self.isWaitingForMoreData = true
return nil
}
let packet = FFMpegPacket()
if avFormatContext.readFrame(into: packet) {
self.lastReadPts = (Int(packet.streamIndex), packet.pts)
return packet
} else {
self.hasReadToEnd = true
return nil
}
}
func readDecodableFrame() -> MediaTrackDecodableFrame? {
while !self.readingError && !self.hasReadToEnd && !self.isWaitingForMoreData {
if let packet = self.readPacketInternal() {
if let stream = self.stream, Int(packet.streamIndex) == stream.info.index {
let packetPts = packet.pts
/*if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value {
self.hasReadToEnd = true
}*/
let pts = CMTimeMake(value: packetPts, timescale: stream.info.timeScale)
let dts = CMTimeMake(value: packet.dts, timescale: stream.info.timeScale)
let duration: CMTime
let frameDuration = packet.duration
if frameDuration != 0 {
duration = CMTimeMake(value: frameDuration * stream.info.timeBase, timescale: stream.info.timeScale)
} else {
duration = stream.info.fps
}
let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration)
return frame
}
} else {
break
}
}
return nil
}
public func readFrame() -> ReadFrameResult {
guard let stream = self.stream else {
return .error
}
while true {
var result: MediaTrackFrame?
switch stream.decoder {
case let .video(decoder):
result = decoder.decode(ptsOffset: nil, forceARGB: false, unpremultiplyAlpha: false, displayImmediately: false)
case let .videoPassthrough(decoder):
result = decoder.decode()
case let .audio(decoder):
result = decoder.decode()
}
if let result {
if self.didSendEndToDecoder {
assert(true)
}
return .frame(result)
}
if !self.isWaitingForMoreData && !self.readingError && !self.hasReadToEnd {
if let decodableFrame = self.readDecodableFrame() {
let _ = stream.decoder.send(frame: decodableFrame)
}
} else if self.hasReadToEnd && !self.didSendEndToDecoder {
self.didSendEndToDecoder = true
let _ = stream.decoder.sendEnd()
} else {
break
}
}
if self.isWaitingForMoreData {
return .waitingForMoreData
} else {
return .endOfStream
}
}
public func updateMaxReadablePts(pts: (streamIndex: Int, pts: Int64, isEnded: Bool)?) {
if self.maxReadablePts?.streamIndex != pts?.streamIndex || self.maxReadablePts?.pts != pts?.pts {
self.maxReadablePts = pts
if let pts {
if pts.isEnded {
self.isWaitingForMoreData = false
} else {
if self.lastReadPts?.streamIndex != pts.streamIndex || self.lastReadPts?.pts != pts.pts {
self.isWaitingForMoreData = false
}
}
} else {
self.isWaitingForMoreData = false
}
}
}
}

View file

@ -490,7 +490,7 @@ final class FFMpegMediaFrameSourceContext: NSObject {
let aspect = Double(metrics.width) / Double(metrics.height)
if self.preferSoftwareDecoding {
if let codec = FFMpegAVCodec.find(forId: codecId) {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
@ -523,7 +523,7 @@ final class FFMpegMediaFrameSourceContext: NSObject {
var codec: FFMpegAVCodec?
if codec == nil {
codec = FFMpegAVCodec.find(forId: codecId)
codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false)
}
if let codec = codec {

View file

@ -277,6 +277,43 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
}
private func convertVideoFrame(_ frame: FFMpegAVFrame, pts: CMTime, dts: CMTime, duration: CMTime, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? {
if frame.nativePixelFormat() == FFMpegAVFrameNativePixelFormat.videoToolbox {
guard let pixelBufferRef = frame.data[3] else {
return nil
}
let unmanagedPixelBuffer = Unmanaged<CVPixelBuffer>.fromOpaque(UnsafeRawPointer(pixelBufferRef))
let pixelBuffer = unmanagedPixelBuffer.takeUnretainedValue()
var formatRef: CMVideoFormatDescription?
let formatStatus = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatRef)
guard let format = formatRef, formatStatus == 0 else {
return nil
}
var timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: pts, decodeTimeStamp: pts)
var sampleBuffer: CMSampleBuffer?
guard CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer) == noErr else {
return nil
}
let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, createIfNecessary: true)! as NSArray
let dict = attachments[0] as! NSMutableDictionary
let resetDecoder = self.resetDecoderOnNextFrame
if self.resetDecoderOnNextFrame {
self.resetDecoderOnNextFrame = false
//dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString as String)
}
if displayImmediately {
dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String)
}
return MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: true)
}
if frame.data[0] == nil {
return nil
}

View file

@ -4,6 +4,7 @@ import CoreMedia
import FFMpegBinding
import VideoToolbox
import Postbox
#if os(macOS)
private let internal_isHardwareAv1Supported: Bool = {
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
@ -96,15 +97,131 @@ public final class ChunkMediaPlayerPartsState {
#endif
public enum MediaDataReaderReadSampleBufferResult {
case frame(CMSampleBuffer)
case waitingForMoreData
case endOfStream
case error
}
public protocol MediaDataReader: AnyObject {
var hasVideo: Bool { get }
var hasAudio: Bool { get }
func readSampleBuffer() -> CMSampleBuffer?
func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult
}
public final class FFMpegMediaDataReader: MediaDataReader {
private let content: ChunkMediaPlayerPart.Content
public final class FFMpegMediaDataReaderV2: MediaDataReader {
public enum Content {
case tempFile(ChunkMediaPlayerPart.TempFile)
case directStream(ChunkMediaPlayerPartsState.DirectReader.Stream)
}
private let content: Content
private let isVideo: Bool
private let videoSource: FFMpegFileReader?
private let audioSource: FFMpegFileReader?
public var hasVideo: Bool {
return self.videoSource != nil
}
public var hasAudio: Bool {
return self.audioSource != nil
}
public init(content: Content, isVideo: Bool, codecName: String?) {
self.content = content
self.isVideo = isVideo
let source: FFMpegFileReader.SourceDescription
var seek: (streamIndex: Int, pts: Int64)?
var maxReadablePts: (streamIndex: Int, pts: Int64, isEnded: Bool)?
switch content {
case let .tempFile(tempFile):
source = .file(tempFile.file.path)
case let .directStream(directStream):
source = .resource(mediaBox: directStream.mediaBox, resource: directStream.resource, size: directStream.size)
seek = (directStream.seek.streamIndex, directStream.seek.pts)
maxReadablePts = directStream.maxReadablePts
}
if self.isVideo {
var passthroughDecoder = true
var useHardwareAcceleration = false
if (codecName == "av1" || codecName == "av01") {
passthroughDecoder = false
useHardwareAcceleration = internal_isHardwareAv1Supported
}
if codecName == "vp9" || codecName == "vp8" {
passthroughDecoder = false
}
/*#if DEBUG
if codecName == "h264" {
passthroughDecoder = false
useHardwareAcceleration = true
}
#endif*/
if let videoSource = FFMpegFileReader(source: source, passthroughDecoder: passthroughDecoder, useHardwareAcceleration: useHardwareAcceleration, selectedStream: .mediaType(.video), seek: seek, maxReadablePts: maxReadablePts) {
self.videoSource = videoSource
} else {
self.videoSource = nil
}
self.audioSource = nil
} else {
if let audioSource = FFMpegFileReader(source: source, passthroughDecoder: false, useHardwareAcceleration: false, selectedStream: .mediaType(.audio), seek: seek, maxReadablePts: maxReadablePts) {
self.audioSource = audioSource
} else {
self.audioSource = nil
}
self.videoSource = nil
}
}
public func update(content: Content) {
guard case let .directStream(directStream) = content else {
return
}
if let audioSource = self.audioSource {
audioSource.updateMaxReadablePts(pts: directStream.maxReadablePts)
} else if let videoSource = self.videoSource {
videoSource.updateMaxReadablePts(pts: directStream.maxReadablePts)
}
}
public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult {
if let videoSource {
switch videoSource.readFrame() {
case let .frame(frame):
return .frame(frame.sampleBuffer)
case .waitingForMoreData:
return .waitingForMoreData
case .endOfStream:
return .endOfStream
case .error:
return .error
}
} else if let audioSource {
switch audioSource.readFrame() {
case let .frame(frame):
return .frame(frame.sampleBuffer)
case .waitingForMoreData:
return .waitingForMoreData
case .endOfStream:
return .endOfStream
case .error:
return .error
}
} else {
return .endOfStream
}
}
}
public final class FFMpegMediaDataReaderV1: MediaDataReader {
private let isVideo: Bool
private let videoSource: SoftwareVideoReader?
private let audioSource: SoftwareAudioSource?
@ -117,42 +234,15 @@ public final class FFMpegMediaDataReader: MediaDataReader {
return self.audioSource != nil
}
public init(content: ChunkMediaPlayerPart.Content, isVideo: Bool, codecName: String?) {
self.content = content
public init(filePath: String, isVideo: Bool, codecName: String?) {
self.isVideo = isVideo
let filePath: String
var focusedPart: MediaStreamFocusedPart?
switch content {
case let .tempFile(tempFile):
filePath = tempFile.file.path
case let .directFile(directFile):
filePath = directFile.path
let stream = isVideo ? directFile.video : directFile.audio
guard let stream else {
self.videoSource = nil
self.audioSource = nil
return
}
focusedPart = MediaStreamFocusedPart(
seekStreamIndex: stream.index,
startPts: stream.startPts,
endPts: stream.endPts
)
}
if self.isVideo {
var passthroughDecoder = true
if (codecName == "av1" || codecName == "av01") && !internal_isHardwareAv1Supported {
passthroughDecoder = false
}
if codecName == "vp9" || codecName == "vp8" {
passthroughDecoder = false
}
let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: passthroughDecoder, focusedPart: focusedPart)
let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: passthroughDecoder)
if videoSource.hasStream {
self.videoSource = videoSource
} else {
@ -160,7 +250,7 @@ public final class FFMpegMediaDataReader: MediaDataReader {
}
self.audioSource = nil
} else {
let audioSource = SoftwareAudioSource(path: filePath, focusedPart: focusedPart)
let audioSource = SoftwareAudioSource(path: filePath)
if audioSource.hasStream {
self.audioSource = audioSource
} else {
@ -170,19 +260,23 @@ public final class FFMpegMediaDataReader: MediaDataReader {
}
}
public func readSampleBuffer() -> CMSampleBuffer? {
public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult {
if let videoSource {
let frame = videoSource.readFrame()
if let frame {
return frame.sampleBuffer
return .frame(frame.sampleBuffer)
} else {
return nil
return .endOfStream
}
} else if let audioSource {
return audioSource.readSampleBuffer()
if let sampleBuffer = audioSource.readSampleBuffer() {
return .frame(sampleBuffer)
} else {
return .endOfStream
}
} else {
return .endOfStream
}
return nil
}
}
@ -225,14 +319,18 @@ public final class AVAssetVideoDataReader: MediaDataReader {
}
}
public func readSampleBuffer() -> CMSampleBuffer? {
public func readSampleBuffer() -> MediaDataReaderReadSampleBufferResult {
guard let mediaInfo = self.mediaInfo, let assetReader = self.assetReader, let assetOutput = self.assetOutput else {
return nil
return .endOfStream
}
var retryCount = 0
while true {
if let sampleBuffer = assetOutput.copyNextSampleBuffer() {
return createSampleBuffer(fromSampleBuffer: sampleBuffer, withTimeOffset: mediaInfo.startTime, duration: nil)
if let convertedSampleBuffer = createSampleBuffer(fromSampleBuffer: sampleBuffer, withTimeOffset: mediaInfo.startTime, duration: nil) {
return .frame(convertedSampleBuffer)
} else {
return .endOfStream
}
} else if assetReader.status == .reading && retryCount < 100 {
Thread.sleep(forTimeInterval: 1.0 / 60.0)
retryCount += 1
@ -241,7 +339,7 @@ public final class AVAssetVideoDataReader: MediaDataReader {
}
}
return nil
return .endOfStream
}
}

View file

@ -163,7 +163,7 @@ public final class SoftwareVideoSource {
break
}
} else {
if let codec = FFMpegAVCodec.find(forId: codecId) {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
@ -382,7 +382,6 @@ private func SoftwareAudioSource_seekCallback(userData: UnsafeMutableRawPointer?
}
public final class SoftwareAudioSource {
private let focusedPart: MediaStreamFocusedPart?
private var readingError = false
private var audioStream: SoftwareAudioStream?
private var avIoContext: FFMpegAVIOContext?
@ -397,11 +396,9 @@ public final class SoftwareAudioSource {
return self.audioStream != nil
}
public init(path: String, focusedPart: MediaStreamFocusedPart?) {
public init(path: String) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
self.focusedPart = focusedPart
var s = stat()
stat(path, &s)
self.size = Int32(s.st_size)
@ -451,7 +448,7 @@ public final class SoftwareAudioSource {
let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
let codec = FFMpegAVCodec.find(forId: codecId)
let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false)
if let codec = codec {
let codecContext = FFMpegAVCodecContext(codec: codec)
@ -466,12 +463,8 @@ public final class SoftwareAudioSource {
self.audioStream = audioStream
if let focusedPart = self.focusedPart {
avFormatContext.seekFrame(forStreamIndex: Int32(focusedPart.seekStreamIndex), pts: focusedPart.startPts.value, positionOnKeyframe: true)
} else {
if let audioStream = self.audioStream {
avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: false)
}
if let audioStream = self.audioStream {
avFormatContext.seekFrame(forStreamIndex: Int32(audioStream.index), pts: 0, positionOnKeyframe: false)
}
}
@ -502,10 +495,6 @@ public final class SoftwareAudioSource {
if let audioStream = self.audioStream, Int(packet.streamIndex) == audioStream.index {
let packetPts = packet.pts
if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value {
self.hasReadToEnd = true
}
let pts = CMTimeMake(value: packetPts, timescale: audioStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: audioStream.timebase.timescale)
@ -579,18 +568,6 @@ public final class SoftwareAudioSource {
}
}
public struct MediaStreamFocusedPart {
public let seekStreamIndex: Int
public let startPts: CMTime
public let endPts: CMTime
public init(seekStreamIndex: Int, startPts: CMTime, endPts: CMTime) {
self.seekStreamIndex = seekStreamIndex
self.startPts = startPts
self.endPts = endPts
}
}
private func SoftwareVideoReader_readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer<UInt8>?, bufferSize: Int32) -> Int32 {
let context = Unmanaged<SoftwareVideoReader>.fromOpaque(userData!).takeUnretainedValue()
if let fd = context.fd {
@ -617,7 +594,6 @@ private func SoftwareVideoReader_seekCallback(userData: UnsafeMutableRawPointer?
}
final class SoftwareVideoReader {
private let focusedPart: MediaStreamFocusedPart?
private var readingError = false
private var videoStream: SoftwareVideoStream?
private var avIoContext: FFMpegAVIOContext?
@ -636,11 +612,9 @@ final class SoftwareVideoReader {
return self.videoStream != nil
}
public init(path: String, hintVP9: Bool, passthroughDecoder: Bool = false, focusedPart: MediaStreamFocusedPart?) {
public init(path: String, hintVP9: Bool, passthroughDecoder: Bool = false) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
self.focusedPart = focusedPart
var s = stat()
stat(path, &s)
self.size = Int32(s.st_size)
@ -721,7 +695,7 @@ final class SoftwareVideoReader {
break
}
} else {
if let codec = FFMpegAVCodec.find(forId: codecId) {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {
@ -737,12 +711,8 @@ final class SoftwareVideoReader {
self.videoStream = videoStream
if let focusedPart = self.focusedPart {
avFormatContext.seekFrame(forStreamIndex: Int32(focusedPart.seekStreamIndex), pts: focusedPart.startPts.value, positionOnKeyframe: true)
} else {
if let videoStream = self.videoStream {
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
}
if let videoStream = self.videoStream {
avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true)
}
}
@ -775,10 +745,6 @@ final class SoftwareVideoReader {
if let videoStream = self.videoStream, Int(packet.streamIndex) == videoStream.index {
let packetPts = packet.pts
if let focusedPart = self.focusedPart, packetPts >= focusedPart.endPts.value {
self.hasReadToEnd = true
}
let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale)
let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale)
@ -958,21 +924,8 @@ public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? {
var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale)
duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime))
var codecName: String?
let codecId = avFormatContext.codecId(atStreamIndex: streamIndex)
if codecId == FFMpegCodecIdMPEG4 {
codecName = "mpeg4"
} else if codecId == FFMpegCodecIdH264 {
codecName = "h264"
} else if codecId == FFMpegCodecIdHEVC {
codecName = "hevc"
} else if codecId == FFMpegCodecIdAV1 {
codecName = "av1"
} else if codecId == FFMpegCodecIdVP9 {
codecName = "vp9"
} else if codecId == FFMpegCodecIdVP8 {
codecName = "vp8"
}
let codecName = resolveFFMpegCodecName(id: codecId)
streamInfos.append((isVideo: isVideo, info: FFMpegMediaInfo.Info(
index: Int(streamIndex),
@ -987,3 +940,21 @@ public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? {
return FFMpegMediaInfo(audio: streamInfos.first(where: { !$0.isVideo })?.info, video: streamInfos.first(where: { $0.isVideo })?.info)
}
public func resolveFFMpegCodecName(id: Int32) -> String? {
if id == FFMpegCodecIdMPEG4 {
return "mpeg4"
} else if id == FFMpegCodecIdH264 {
return "h264"
} else if id == FFMpegCodecIdHEVC {
return "hevc"
} else if id == FFMpegCodecIdAV1 {
return "av1"
} else if id == FFMpegCodecIdVP9 {
return "vp9"
} else if id == FFMpegCodecIdVP8 {
return "vp8"
} else {
return nil
}
}

View file

@ -146,7 +146,6 @@ private final class UniversalSoftwareVideoSourceImpl {
self.size = sizeValue
}
self.mediaBox = mediaBox
self.source = source
self.automaticallyFetchHeader = automaticallyFetchHeader
@ -210,7 +209,7 @@ private final class UniversalSoftwareVideoSourceImpl {
let rotationAngle: Double = metrics.rotationAngle
let aspect = Double(metrics.width) / Double(metrics.height)
if let codec = FFMpegAVCodec.find(forId: codecId) {
if let codec = FFMpegAVCodec.find(forId: codecId, preferHardwareAccelerationCapable: false) {
let codecContext = FFMpegAVCodecContext(codec: codec)
if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) {
if codecContext.open() {

View file

@ -112,7 +112,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
let arguments = arguments as! PaymentMethodListScreenArguments
switch self {
case let .header(text):
return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "Invite", sectionId: self.section)
return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: NSAttributedString(string: text), animationName: "Invite", sectionId: self.section)
case let .methodsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addMethod(text):

View file

@ -281,7 +281,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
let mediaManager = self.context.sharedContext.mediaManager
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay)
let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay)
videoNode.isUserInteractionEnabled = false
videoNode.isHidden = true
self.videoStartTimestamp = video.representation.startTimestamp

View file

@ -366,7 +366,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
}
let mediaManager = self.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
videoNode.isUserInteractionEnabled = false
videoNode.canAttachContent = true
videoNode.isHidden = true

View file

@ -158,7 +158,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable {
case let .participant(participant, label, revealActions, revealed, enabled):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: label, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil)
} else if let presence = participant.presences[participant.peer.id], self.addIcon {
status = .presence(EnginePeer.Presence(presence), dateTimeFormat)
} else {

View file

@ -129,7 +129,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable {
case let .peer(_, participant, editing, label, enabled, isChannel, isContact):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: label, multiline: false, isActive: false, icon: nil)
status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil)
} else if participant.peer.id != context.account.peerId {
let presence = participant.presences[participant.peer.id] ?? TelegramUserPresence(status: .none, lastActivity: 0)
status = .presence(EnginePeer.Presence(presence), presentationData.dateTimeFormat)

View file

@ -705,6 +705,17 @@ public final class MediaBox {
}
}
public func internal_resourceDataIsCached(id: MediaResourceId, size: Int64, in range: Range<Int64>) -> Bool {
let paths = self.storePathsForId(id)
if let _ = fileSize(paths.complete) {
return true
} else {
let tempManager = MediaBoxFileManager(queue: nil)
return MediaBoxPartialFile.internal_isPartialDataCached(manager: tempManager, path: paths.partial, metaPath: paths.partial + ".meta", range: range)
}
}
public func resourceData(id: MediaResourceId, size: Int64, in range: Range<Int64>, mode: ResourceDataRangeMode = .complete, notifyAboutIncomplete: Bool = false, attemptSynchronously: Bool = false) -> Signal<(Data, Bool), NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()

View file

@ -106,6 +106,16 @@ final class MediaBoxPartialFile {
return (fd, Int(clippedRange.upperBound - clippedRange.lowerBound))
}
static func internal_isPartialDataCached(manager: MediaBoxFileManager, path: String, metaPath: String, range: Range<Int64>) -> Bool {
guard let fileMap = try? MediaBoxFileMap.read(manager: manager, path: metaPath) else {
return false
}
guard let _ = fileMap.contains(range) else {
return false
}
return true
}
var storedSize: Int64 {
assert(self.queue.isCurrent())
return self.fileMap.sum

View file

@ -513,7 +513,11 @@ private final class TimeBasedCleanupImpl {
}
func touch(paths: [String]) {
self.scheduledTouches.append(contentsOf: paths)
for path in paths {
if !self.scheduledTouches.contains(path) {
self.scheduledTouches.append(path)
}
}
self.scheduleTouches()
}

View file

@ -233,7 +233,7 @@ private final class PhoneView: UIView {
hintDimensions: CGSize(width: 1170, height: 1754),
storeAfterDownload: nil
)
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
videoNode.canAttachContent = true
self.videoNode = videoNode

View file

@ -117,7 +117,7 @@ private enum DeleteAccountDataEntry: ItemListNodeEntry, Equatable {
let arguments = arguments as! DeleteAccountDataArguments
switch self {
case let .header(theme, animation, title, text, hideOnSmallScreens):
return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: text, animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil)
return InviteLinkHeaderItem(context: arguments.context, theme: theme, title: title, text: NSAttributedString(string: text), animationName: animation, hideOnSmallScreens: hideOnSmallScreens, sectionId: self.section, linkAction: nil)
case let .peers(_, peers):
return DeleteAccountPeersItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, peers: peers, sectionId: self.section)
case let .info(_, text):

View file

@ -277,20 +277,25 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte
}))
if let postbox, let mediaManager = environment.mediaManager, let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let _ = postbox
let _ = mediaManager
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let _ = decoration
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let _ = videoContent
let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
/*let videoNode = UniversalVideoNode(accountId: AccountRecordId(rawValue: 0), postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
videoNode.alpha = 0.01
self.videoNode = videoNode
self.addSubnode(videoNode)
videoNode.canAttachContent = true
videoNode.play()
videoNode.play()*/
}
}

View file

@ -336,7 +336,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
theme: item.presentationData.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: item.context, theme: item.presentationData.theme, peer: item.transaction.peer, photo: nil, media: [], uniqueGift: nil, backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in

View file

@ -234,8 +234,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1135897376] = { return Api.DefaultHistoryTTL.parse_defaultHistoryTTL($0) }
dict[-712374074] = { return Api.Dialog.parse_dialog($0) }
dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) }
dict[1605718587] = { return Api.DialogFilter.parse_dialogFilter($0) }
dict[-1612542300] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) }
dict[-1438177711] = { return Api.DialogFilter.parse_dialogFilter($0) }
dict[-1772913705] = { return Api.DialogFilter.parse_dialogFilterChatlist($0) }
dict[909284270] = { return Api.DialogFilter.parse_dialogFilterDefault($0) }
dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) }
dict[-445792507] = { return Api.DialogPeer.parse_dialogPeer($0) }
@ -1226,7 +1226,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1044107055] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultAdsHidden($0) }
dict[-2073059774] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultChooseOption($0) }
dict[-1384544183] = { return Api.channels.SponsoredMessageReportResult.parse_sponsoredMessageReportResultReported($0) }
dict[500007837] = { return Api.chatlists.ChatlistInvite.parse_chatlistInvite($0) }
dict[-250687953] = { return Api.chatlists.ChatlistInvite.parse_chatlistInvite($0) }
dict[-91752871] = { return Api.chatlists.ChatlistInvite.parse_chatlistInviteAlready($0) }
dict[-1816295539] = { return Api.chatlists.ChatlistUpdates.parse_chatlistUpdates($0) }
dict[283567014] = { return Api.chatlists.ExportedChatlistInvite.parse_exportedChatlistInvite($0) }

View file

@ -476,17 +476,17 @@ public extension Api.channels {
}
public extension Api.chatlists {
enum ChatlistInvite: TypeConstructorDescription {
case chatlistInvite(flags: Int32, title: String, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
case chatlistInvite(flags: Int32, title: Api.TextWithEntities, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
case chatlistInviteAlready(filterId: Int32, missingPeers: [Api.Peer], alreadyPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .chatlistInvite(let flags, let title, let emoticon, let peers, let chats, let users):
if boxed {
buffer.appendInt32(500007837)
buffer.appendInt32(-250687953)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
title.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(peers.count))
@ -545,8 +545,10 @@ public extension Api.chatlists {
public static func parse_chatlistInvite(_ reader: BufferReader) -> ChatlistInvite? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _2: Api.TextWithEntities?
if let signature = reader.readInt32() {
_2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
}
var _3: String?
if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
var _4: [Api.Peer]?

View file

@ -1166,19 +1166,19 @@ public extension Api {
}
public extension Api {
enum DialogFilter: TypeConstructorDescription {
case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer])
case dialogFilterChatlist(flags: Int32, id: Int32, title: String, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer])
case dialogFilter(flags: Int32, id: Int32, title: Api.TextWithEntities, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer])
case dialogFilterChatlist(flags: Int32, id: Int32, title: Api.TextWithEntities, emoticon: String?, color: Int32?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer])
case dialogFilterDefault
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .dialogFilter(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers, let excludePeers):
if boxed {
buffer.appendInt32(1605718587)
buffer.appendInt32(-1438177711)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
title.serialize(buffer, true)
if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
@ -1199,11 +1199,11 @@ public extension Api {
break
case .dialogFilterChatlist(let flags, let id, let title, let emoticon, let color, let pinnedPeers, let includePeers):
if boxed {
buffer.appendInt32(-1612542300)
buffer.appendInt32(-1772913705)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
title.serialize(buffer, true)
if Int(flags) & Int(1 << 25) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 27) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
@ -1242,8 +1242,10 @@ public extension Api {
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: String?
_3 = parseString(reader)
var _3: Api.TextWithEntities?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
}
var _4: String?
if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) }
var _5: Int32?
@ -1280,8 +1282,10 @@ public extension Api {
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
var _3: String?
_3 = parseString(reader)
var _3: Api.TextWithEntities?
if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
}
var _4: String?
if Int(_1!) & Int(1 << 25) != 0 {_4 = parseString(reader) }
var _5: Int32?

View file

@ -36,8 +36,6 @@ public final class PresentationCallImpl: PresentationCall {
public let preferredVideoCodec: String?
public let peer: EnginePeer?
private let isExpectedToBeConference: Bool
private let serializedData: String?
private let dataSaving: VoiceCallDataSaving
private let proxyServer: ProxyServerSettings?
@ -213,8 +211,6 @@ public final class PresentationCallImpl: PresentationCall {
self.currentNetworkType = currentNetworkType
self.updatedNetworkType = updatedNetworkType
self.isExpectedToBeConference = self.context.sharedContext.immediateExperimentalUISettings.conferenceCalls
var didReceiveAudioOutputs = false
var callSessionState: Signal<CallSession, NoError> = .complete()
@ -326,18 +322,6 @@ public final class PresentationCallImpl: PresentationCall {
self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in
}
}
if self.isExpectedToBeConference {
self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in
Queue.mainQueue().async {
guard let self else {
return
}
self.processConferenceSignalingData(dataList: dataList)
}
})
}
}
deinit {
@ -554,6 +538,14 @@ public final class PresentationCallImpl: PresentationCall {
presentationState = PresentationCallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel)
case let .active(_, _, keyVisualHash, _, _, _, _, _, _), let .switchedToConference(_, keyVisualHash, _):
self.callWasActive = true
var isConference = false
if case let .active(_, _, _, _, _, version, _, _, _) = sessionState.state {
isConference = version == "13.0.0"
} else if case .switchedToConference = sessionState.state {
isConference = true
}
if let callContextState = callContextState {
switch callContextState.state {
case .initializing:
@ -580,7 +572,7 @@ public final class PresentationCallImpl: PresentationCall {
}
presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel)
}
} else if !self.isExpectedToBeConference {
} else if !isConference {
presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel)
}
}
@ -668,8 +660,7 @@ public final class PresentationCallImpl: PresentationCall {
conferenceCall.hasActiveIncomingData
)
|> map { remoteConferenceIsConnected, hasActiveIncomingData -> Bool in
//return remoteConferenceIsConnected || hasActiveIncomingData
return true
return remoteConferenceIsConnected || hasActiveIncomingData
}
|> distinctUntilChanged
@ -764,7 +755,7 @@ public final class PresentationCallImpl: PresentationCall {
}
if !self.didDropCall {
self.didDropCall = true
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil))
}
})
@ -795,6 +786,28 @@ public final class PresentationCallImpl: PresentationCall {
strongSelf.audioLevelPromise.set(level)
}
})
let localIsConnected = conferenceCall.state
|> map { state -> Bool in
switch state.networkState {
case .connected:
return true
default:
return false
}
}
|> distinctUntilChanged
let bothLocalAndRemoteConnected = combineLatest(queue: .mainQueue(),
localIsConnected,
remoteIsConnectedAggregated
)
|> map { localIsConnected, remoteIsConnectedAggregated -> Bool in
return localIsConnected && remoteIsConnectedAggregated
}
|> distinctUntilChanged
conferenceCall.internal_isRemoteConnected.set(bothLocalAndRemoteConnected)
})
}
}
@ -805,13 +818,25 @@ public final class PresentationCallImpl: PresentationCall {
self.audioSessionShouldBeActive.set(true)
}
case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall):
if conferenceCall == nil, self.isExpectedToBeConference {
if conferenceCall == nil, version == "13.0.0" {
self.createConferenceIfPossible()
}
self.audioSessionShouldBeActive.set(true)
if self.isExpectedToBeConference || conferenceCallData != nil {
if version == "13.0.0" && self.conferenceSignalingDataDisposable == nil {
self.conferenceSignalingDataDisposable = self.context.account.callSessionManager.beginReceivingCallSignalingData(internalId: self.internalId, { [weak self] dataList in
Queue.mainQueue().async {
guard let self else {
return
}
self.processConferenceSignalingData(dataList: dataList)
}
})
}
if version == "13.0.0" || conferenceCallData != nil {
if sessionState.isOutgoing {
self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date())
}
@ -1112,9 +1137,9 @@ public final class PresentationCallImpl: PresentationCall {
}
private func sendConferenceSignalingMessage(dict: [String: Any]) {
/*if let data = try? JSONSerialization.data(withJSONObject: dict) {
if let data = try? JSONSerialization.data(withJSONObject: dict) {
self.context.account.callSessionManager.sendSignalingData(internalId: self.internalId, data: data)
}*/
}
}
private func updateIsAudioSessionActive(_ value: Bool) {

View file

@ -871,6 +871,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let conferenceFromCallId: CallId?
private let isConference: Bool
var internal_isRemoteConnected = Promise<Bool>()
private var internal_isRemoteConnectedDisposable: Disposable?
public var onMutedSpeechActivityDetected: ((Bool) -> Void)?
init(
@ -1228,6 +1231,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.screencastFramesDisposable?.dispose()
self.screencastAudioDataDisposable?.dispose()
self.screencastStateDisposable?.dispose()
self.internal_isRemoteConnectedDisposable?.dispose()
}
private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) {
@ -1712,9 +1717,6 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
var encryptionKey: Data?
encryptionKey = self.encryptionKey?.key
if "".isEmpty {
encryptionKey = nil
}
genericCallContext = .call(OngoingGroupCallContext(audioSessionActive: self.audioSessionActive.get(), video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in
let disposable = MetaDisposable()
@ -1786,6 +1788,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})
self.signalBarsPromise.set(callContext.signalBars)
self.internal_isRemoteConnectedDisposable = (self.internal_isRemoteConnected.get()
|> distinctUntilChanged
|> deliverOnMainQueue).startStrict(next: { [weak callContext] isRemoteConnected in
guard let callContext else {
return
}
callContext.addRemoteConnectedEvent(isRemoteConntected: isRemoteConnected)
})
}
}
@ -2365,22 +2376,28 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if let muteState = filteredMuteState {
if muteState.canUnmute {
switch strongSelf.isMutedValue {
case let .muted(isPushToTalkActive):
if !isPushToTalkActive {
strongSelf.genericCallContext?.setIsMuted(true)
}
case .unmuted:
if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute {
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
strongSelf.isMutedPromise.set(strongSelf.isMutedValue)
strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
strongSelf.genericCallContext?.setIsMuted(true)
} else {
switch strongSelf.isMutedValue {
case .muted:
break
case .unmuted:
let _ = strongSelf.updateMuteState(peerId: strongSelf.joinAsPeerId, isMuted: false)
}
}
} else {
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
strongSelf.isMutedPromise.set(strongSelf.isMutedValue)
strongSelf.genericCallContext?.setIsMuted(true)
strongSelf.stateValue.muteState = muteState
}
strongSelf.stateValue.muteState = muteState
} else if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute {
strongSelf.isMutedValue = .muted(isPushToTalkActive: false)
strongSelf.isMutedPromise.set(strongSelf.isMutedValue)
strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false)
strongSelf.genericCallContext?.setIsMuted(true)
}

View file

@ -419,7 +419,12 @@ final class VideoChatParticipantVideoComponent: Component {
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
}
let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription)
let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
if component.isMyPeer && component.isPresentation {
videoDescription = nil
} else {
videoDescription = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription)
}
var isEffectivelyPaused = false
if let videoDescription, videoDescription.isPaused {

View file

@ -152,16 +152,15 @@ extension VideoChatScreenComponent.View {
}
}
//TODO:localize
let qualityList: [(Int, String)] = [
(0, "Audio Only"),
(0, environment.strings.VideoChat_IncomingVideoQuality_AudioOnly),
(180, "180p"),
(360, "360p"),
(Int.max, "720p")
]
let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? ""
items.append(.action(ContextMenuActionItem(text: "Receive Video Quality", textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in
items.append(.action(ContextMenuActionItem(text: environment.strings.VideoChat_IncomingVideoQuality_Title, textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in
return nil
}, action: { [weak self] c, _ in
guard let self else {

View file

@ -132,6 +132,7 @@ enum AccountStateMutationOperation {
case UpdateStarsBalance(peerId: PeerId, balance: Api.StarsAmount)
case UpdateStarsRevenueStatus(peerId: PeerId, status: StarsRevenueStats.Balances)
case UpdateStarsReactionsAreAnonymousByDefault(isAnonymous: Bool)
case ReportMessageDelivery([MessageId])
}
struct HoleFromPreviousState {
@ -702,9 +703,13 @@ struct AccountMutableState {
self.addOperation(.UpdateStarsReactionsAreAnonymousByDefault(isAnonymous: isAnonymous))
}
mutating func addReportMessageDelivery(messageIds: [MessageId]) {
self.addOperation(.ReportMessageDelivery(messageIds))
}
mutating func addOperation(_ operation: AccountStateMutationOperation) {
switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault:
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault, .ReportMessageDelivery:
break
case let .AddMessages(messages, location):
for message in messages {
@ -852,6 +857,7 @@ struct AccountReplayedFinalState {
let updatedStarsBalance: [PeerId: StarsAmount]
let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances]
let sentScheduledMessageIds: Set<MessageId>
let reportMessageDelivery: Set<MessageId>
}
struct AccountFinalStateEvents {
@ -882,12 +888,13 @@ struct AccountFinalStateEvents {
let updatedRevenueBalances: [PeerId: RevenueStats.Balances]
let updatedStarsBalance: [PeerId: StarsAmount]
let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances]
let reportMessageDelivery: Set<MessageId>
var isEmpty: Bool {
return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty
return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty && self.reportMessageDelivery.isEmpty
}
init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set<PeerId> = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set<MessageId> = Set()) {
init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set<PeerId> = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: StarsAmount] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set<MessageId> = Set(), reportMessageDelivery: Set<MessageId> = Set()) {
self.addedIncomingMessageIds = addedIncomingMessageIds
self.addedReactionEvents = addedReactionEvents
self.wasScheduledMessageIds = wasScheduledMessageIds
@ -915,6 +922,7 @@ struct AccountFinalStateEvents {
self.updatedStarsBalance = updatedStarsBalance
self.updatedStarsRevenueStatus = updatedStarsRevenueStatus
self.sentScheduledMessageIds = sentScheduledMessageIds
self.reportMessageDelivery = reportMessageDelivery
}
init(state: AccountReplayedFinalState) {
@ -945,6 +953,7 @@ struct AccountFinalStateEvents {
self.updatedStarsBalance = state.updatedStarsBalance
self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus
self.sentScheduledMessageIds = state.sentScheduledMessageIds
self.reportMessageDelivery = state.reportMessageDelivery
}
func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents {
@ -977,6 +986,9 @@ struct AccountFinalStateEvents {
var sentScheduledMessageIds = self.sentScheduledMessageIds
sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds)
return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds)
var reportMessageDelivery = self.reportMessageDelivery
reportMessageDelivery.formUnion(other.reportMessageDelivery)
return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds, reportMessageDelivery: reportMessageDelivery)
}
}

View file

@ -171,7 +171,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId))
case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId):
return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId)))
case let .messageActionStarGift(flags, apiGift, message, convertStars, _, upgradeStars):
case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars):
let text: String?
let entities: [MessageTextEntity]?
switch message {
@ -185,7 +185,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0))
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, upgradeMessageId: upgradeMessageId))
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil

View file

@ -721,6 +721,7 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network:
updatedState.mergeChats(chats)
updatedState.mergeUsers(users)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
for message in messages {
if let preCachedResources = message.preCachedResources {
for (resource, data) in preCachedResources {
@ -738,6 +739,10 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network:
}
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) {
updatedState.addMessages([message], location: .UpperHistoryBlock)
if let reportDeliveryAttribute = message.attributes.first(where: { $0 is ReportDeliveryMessageAttribute }) as? ReportDeliveryMessageAttribute, case let .Id(id) = message.id, reportDeliveryAttribute.untilDate > currentTime {
updatedState.addReportMessageDelivery(messageIds: [id])
}
}
}
@ -901,6 +906,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
}
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var missingUpdatesFromChannels = Set<PeerId>()
for update in sortedUpdates(updates) {
@ -1110,6 +1117,10 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
}
}
updatedState.addMessages([message], location: .UpperHistoryBlock)
if let reportDeliveryAttribute = message.attributes.first(where: { $0 is ReportDeliveryMessageAttribute }) as? ReportDeliveryMessageAttribute, case let .Id(id) = message.id, reportDeliveryAttribute.untilDate > currentTime {
updatedState.addReportMessageDelivery(messageIds: [id])
}
}
case let .updateServiceNotification(flags, date, type, text, media, entities):
let popup = (flags & (1 << 0)) != 0
@ -3282,7 +3293,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddQuickReplyMessages: OptimizeAddMessagesState?
for operation in operations {
switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault:
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsAreAnonymousByDefault, .ReportMessageDelivery:
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
}
@ -3421,6 +3432,7 @@ func replayFinalState(
var updatedStarsBalance: [PeerId: StarsAmount] = [:]
var updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:]
var updatedStarsReactionsAreAnonymousByDefault: Bool?
var reportMessageDelivery = Set<MessageId>()
var holesFromPreviousStateMessageIds: [MessageId] = []
var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:]
@ -4855,6 +4867,8 @@ func replayFinalState(
updatedStarsRevenueStatus[peerId] = status
case let .UpdateStarsReactionsAreAnonymousByDefault(value):
updatedStarsReactionsAreAnonymousByDefault = value
case let .ReportMessageDelivery(messageIds):
reportMessageDelivery = Set(messageIds)
}
}
@ -5376,6 +5390,7 @@ func replayFinalState(
updatedRevenueBalances: updatedRevenueBalances,
updatedStarsBalance: updatedStarsBalance,
updatedStarsRevenueStatus: updatedStarsRevenueStatus,
sentScheduledMessageIds: finalState.state.sentScheduledMessageIds
sentScheduledMessageIds: finalState.state.sentScheduledMessageIds,
reportMessageDelivery: reportMessageDelivery
)
}

View file

@ -352,6 +352,7 @@ public final class AccountStateManager {
private let appliedMaxMessageIdDisposable = MetaDisposable()
private let appliedQtsPromise = Promise<Int32?>(nil)
private let appliedQtsDisposable = MetaDisposable()
private let reportMessageDeliveryDisposable = DisposableSet()
let updateConfigRequested: (() -> Void)?
let isPremiumUpdated: (() -> Void)?
@ -391,6 +392,7 @@ public final class AccountStateManager {
self.operationDisposable.dispose()
self.appliedMaxMessageIdDisposable.dispose()
self.appliedQtsDisposable.dispose()
self.reportMessageDeliveryDisposable.dispose()
}
public func reset() {
@ -1130,6 +1132,9 @@ public final class AccountStateManager {
if !events.sentScheduledMessageIds.isEmpty {
strongSelf.sentScheduledMessageIdsPipe.putNext(events.sentScheduledMessageIds)
}
if !events.reportMessageDelivery.isEmpty {
strongSelf.reportMessageDeliveryDisposable.add(_internal_reportMessageDelivery(postbox: strongSelf.postbox, network: strongSelf.network, messageIds: Array(events.reportMessageDelivery), fromPushNotification: false).start())
}
if !events.isContactUpdates.isEmpty {
strongSelf.addIsContactUpdates(events.isContactUpdates)
}

View file

@ -4,24 +4,20 @@ import TelegramApi
public final class ReportDeliveryMessageAttribute: Equatable, MessageAttribute {
public let untilDate: Int32
public let isReported: Bool
public init(untilDate: Int32, isReported: Bool) {
self.untilDate = untilDate
self.isReported = isReported
}
required public init(decoder: PostboxDecoder) {
self.untilDate = decoder.decodeInt32ForKey("d", orElse: 0)
self.isReported = decoder.decodeBoolForKey("r", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.untilDate, forKey: "d")
encoder.encodeBool(self.isReported, forKey: "r")
}
public static func ==(lhs: ReportDeliveryMessageAttribute, rhs: ReportDeliveryMessageAttribute) -> Bool {
return lhs.untilDate == rhs.untilDate && lhs.isReported == rhs.isReported
return lhs.untilDate == rhs.untilDate
}
}

View file

@ -130,7 +130,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String)
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool)
public init(decoder: PostboxDecoder) {
@ -253,7 +253,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
}
self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId)
case 44:
self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false))
self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"))
case 45:
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false))
default:
@ -548,7 +548,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "giveawayMsgId")
}
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded):
case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, upgradeMessageId):
encoder.encodeInt32(44, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
if let convertStars {
@ -574,6 +574,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeNil(forKey: "upgradeStars")
}
encoder.encodeBool(isRefunded, forKey: "isRefunded")
if let upgradeMessageId {
encoder.encodeInt32(upgradeMessageId, forKey: "upgradeMessageId")
} else {
encoder.encodeNil(forKey: "upgradeMessageId")
}
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")

View file

@ -0,0 +1,33 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
public func _internal_reportMessageDelivery(postbox: Postbox, network: Network, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal<Never, NoError> {
var signals: [Signal<Void, NoError>] = []
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
signals.append(_internal_reportMessageDeliveryByPeerId(postbox: postbox, network: network, peerId: peerId, messageIds: messageIds, fromPushNotification: fromPushNotification))
}
return combineLatest(signals)
|> ignoreValues
}
private func _internal_reportMessageDeliveryByPeerId(postbox: Postbox, network: Network, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], fromPushNotification: Bool) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else {
return .complete()
}
var flags: Int32 = 0
if fromPushNotification {
flags |= (1 << 0)
}
return network.request(Api.functions.messages.reportMessagesDelivery(flags: flags, peer: inputPeer, id: messageIds.map { $0.id }))
|> `catch` { error -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ in
return .complete()
}
}
|> switchToLatest
}

View file

@ -175,6 +175,7 @@ extension BotPaymentMethod {
public enum BotPaymentFormRequestError {
case generic
case alreadyActive
case noPaymentNeeded
}
extension BotPaymentInvoice {
@ -457,7 +458,10 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw
}
return network.request(Api.functions.payments.getPaymentForm(flags: flags, invoice: invoice, themeParams: serializedThemeParams))
|> `catch` { _ -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
|> `catch` { error -> Signal<Api.payments.PaymentForm, BotPaymentFormRequestError> in
if error.errorDescription == "NO_PAYMENT_NEEDED" {
return .fail(.noPaymentNeeded)
}
return .fail(.generic)
}
|> mapToSignal { result -> Signal<BotPaymentForm, BotPaymentFormRequestError> in
@ -622,7 +626,7 @@ public enum SendBotPaymentFormError {
}
public enum SendBotPaymentResult {
case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?)
case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?, uniqueStarGift: ProfileGiftsContext.State.StarGift?)
case externalVerificationRequired(url: String)
}
@ -671,12 +675,12 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
case .starsChatSubscription:
let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }
if let first = chats.first {
return .done(receiptMessageId: nil, subscriptionPeerId: first.id)
return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil)
}
default:
break
}
for apiMessage in updates.messages {
if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
@ -721,7 +725,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa
}
}
}
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil)
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: nil)
case let .paymentVerificationNeeded(url):
return .externalVerificationRequired(url: url)
}

View file

@ -689,15 +689,23 @@ func _internal_transferStarGift(account: Account, prepaid: Bool, messageId: Engi
} else {
let source: BotPaymentInvoiceSource = .starGiftTransfer(messageId: messageId, toPeerId: peerId)
return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil)
|> mapError { _ -> TransferStarGiftError in
return .generic
|> map(Optional.init)
|> `catch` { error -> Signal<BotPaymentForm?, TransferStarGiftError> in
if case .noPaymentNeeded = error {
return .single(nil)
}
return .fail(.generic)
}
|> mapToSignal { paymentForm in
return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source)
|> mapError { _ -> TransferStarGiftError in
return .generic
if let paymentForm {
return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source)
|> mapError { _ -> TransferStarGiftError in
return .generic
}
|> ignoreValues
} else {
return _internal_transferStarGift(account: account, prepaid: true, messageId: messageId, peerId: peerId)
}
|> ignoreValues
}
}
}
@ -714,8 +722,12 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, messageId: Engi
|> mapError { _ -> UpgradeStarGiftError in
return .generic
}
|> mapToSignal { _ in
return .complete()
|> mapToSignal { result in
if case let .done(_, _, gift) = result, let gift {
return .single(gift)
} else {
return .complete()
}
}
} else {
var flags: Int32 = 0
@ -970,16 +982,30 @@ private final class ProfileGiftsContextImpl {
self.pushState()
}
func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) {
self.actionDisposable.set(
_internal_upgradeStarGift(account: self.account, formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in
guard let self else {
return
}
let _ = self
})
)
self.pushState()
func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
return Signal { [weak self] subscriber in
guard let self else {
return EmptyDisposable
}
let disposable = MetaDisposable()
disposable.set(
_internal_upgradeStarGift(account: self.account, formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in
guard let self else {
return
}
if let index = self.gifts.firstIndex(where: { $0.messageId == messageId }) {
self.gifts[index] = result
self.pushState()
}
subscriber.putNext(result)
}, error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
})
)
return disposable
}
}
private func pushState() {
@ -1186,9 +1212,19 @@ public final class ProfileGiftsContext {
}
}
public func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) {
self.impl.with { impl in
impl.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo)
public func upgradeStarGift(formId: Int64?, messageId: EngineMessage.Id, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.upgradeStarGift(formId: formId, messageId: messageId, keepOriginalInfo: keepOriginalInfo).start(next: { value in
subscriber.putNext(value)
}, error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}

View file

@ -1418,12 +1418,13 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
case .starsChatSubscription:
let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }
if let first = chats.first {
return .done(receiptMessageId: nil, subscriptionPeerId: first.id)
return .done(receiptMessageId: nil, subscriptionPeerId: first.id, uniqueStarGift: nil)
}
default:
break
}
var receiptMessageId: MessageId?
var resultGift: ProfileGiftsContext.State.StarGift?
for apiMessage in updates.messages {
if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
@ -1463,12 +1464,28 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer:
receiptMessageId = nil
}
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _) = action.action, case let .Id(messageId) = message.id {
resultGift = ProfileGiftsContext.State.StarGift(
gift: gift,
fromPeer: nil,
date: message.timestamp,
text: nil,
entities: nil,
messageId: messageId,
nameHidden: false,
savedToProfile: savedToProfile,
convertStars: nil,
canUpgrade: false,
canExportDate: canExportDate,
upgradeStars: nil,
transferStars: transferStars
)
}
}
}
}
}
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil)
return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil, uniqueStarGift: resultGift)
case let .paymentVerificationNeeded(url):
return .externalVerificationRequired(url: url)
}

View file

@ -342,9 +342,17 @@ extension ChatListFilter {
case .dialogFilterDefault:
self = .allChats
case let .dialogFilter(flags, id, title, emoticon, color, pinnedPeers, includePeers, excludePeers):
let titleText: String
let titleEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(text, entities):
titleText = text
titleEntities = messageTextEntitiesFromApiEntities(entities)
}
let disableTitleAnimations = (flags & (1 << 28)) != 0
self = .filter(
id: id,
title: ChatFolderTitle(text: title, entities: [], enableAnimations: true),
title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimations),
emoticon: emoticon,
data: ChatListFilterData(
isShared: false,
@ -392,9 +400,18 @@ extension ChatListFilter {
)
)
case let .dialogFilterChatlist(flags, id, title, emoticon, color, pinnedPeers, includePeers):
let titleText: String
let titleEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(text, entities):
titleText = text
titleEntities = messageTextEntitiesFromApiEntities(entities)
}
let disableTitleAnimations = (flags & (1 << 28)) != 0
self = .filter(
id: id,
title: ChatFolderTitle(text: title, entities: [], enableAnimations: true),
title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimations),
emoticon: emoticon,
data: ChatListFilterData(
isShared: true,
@ -446,7 +463,10 @@ extension ChatListFilter {
if data.color != nil {
flags |= 1 << 27
}
return .dialogFilterChatlist(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
if !title.enableAnimations {
flags |= 1 << 28
}
return .dialogFilterChatlist(flags: flags, id: id, title: .textWithEntities(text: title.text, entities: apiEntitiesFromMessageTextEntities(title.entities, associatedPeers: SimpleDictionary())), emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {
@ -472,7 +492,10 @@ extension ChatListFilter {
if data.color != nil {
flags |= 1 << 27
}
return .dialogFilter(flags: flags, id: id, title: title.text, emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
if !title.enableAnimations {
flags |= 1 << 28
}
return .dialogFilter(flags: flags, id: id, title: .textWithEntities(text: title.text, entities: apiEntitiesFromMessageTextEntities(title.entities, associatedPeers: SimpleDictionary())), emoticon: emoticon, color: data.color?.rawValue, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {

View file

@ -273,9 +273,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|> mapToSignal { result -> Signal<ChatFolderLinkContents, CheckChatFolderLinkError> in
return account.postbox.transaction { transaction -> ChatFolderLinkContents in
switch result {
case let .chatlistInvite(_, title, emoticon, peers, chats, users):
case let .chatlistInvite(flags, title, emoticon, peers, chats, users):
let _ = emoticon
let disableTitleAnimation = (flags & (1 << 1)) != 0
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
var memberCounts: [PeerId: Int] = [:]
@ -301,7 +303,15 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
}
}
return ChatFolderLinkContents(localFilterId: nil, title: ChatFolderTitle(text: title, entities: [], enableAnimations: true), peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
let titleText: String
let titleEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(text, entities):
titleText = text
titleEntities = messageTextEntitiesFromApiEntities(entities)
}
return ChatFolderLinkContents(localFilterId: nil, title: ChatFolderTitle(text: titleText, entities: titleEntities, enableAnimations: !disableTitleAnimation), peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
case let .chatlistInviteAlready(filterId, missingPeers, alreadyPeers, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
var memberCounts: [PeerId: Int] = [:]

View file

@ -1066,7 +1066,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, _):
case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, _):
if !forAdditionalServiceMessage {
if let text {
let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()))

View file

@ -151,7 +151,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
continuePlayingWithoutSoundOnLostAudioSession: false,
storeAfterDownload: nil
)
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
videoNode.canAttachContent = true
self.videoNode = videoNode

View file

@ -277,7 +277,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
if videoContent.id != strongSelf.videoContent?.id {
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in
if let strongSelf = self {

View file

@ -467,7 +467,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false
}
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, _, upgradeStars, isRefunded):
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, _, upgradeStars, isRefunded, _):
if case let .generic(gift) = gift {
isStarGift = true
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
@ -488,7 +488,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle_Displaying
}
} else {
if let convertStars {
if let convertStars, convertStars > 0 {
text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string
} else {
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle
@ -500,16 +500,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
peerName = EnginePeer(peer).compactDisplayTitle
}
if peerName.isEmpty {
if let convertStars {
if let convertStars, convertStars > 0 {
text = item.presentationData.strings.Notification_StarGift_Subtitle(item.presentationData.strings.Notification_StarGift_Subtitle_Stars(Int32(convertStars))).string
} else {
text = item.presentationData.strings.Notification_StarGift_Bot_Subtitle
}
} else {
let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars ?? 0)))
text = formattedString.string
if let starsRange = formattedString.ranges.last {
entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold))
if let convertStars, convertStars > 0 {
let formattedString = item.presentationData.strings.Notification_StarGift_Subtitle_Other(peerName, item.presentationData.strings.Notification_StarGift_Subtitle_Other_Stars(Int32(convertStars)))
text = formattedString.string
if let starsRange = formattedString.ranges.last {
entities.append(MessageTextEntity(range: starsRange.range.lowerBound ..< starsRange.range.upperBound, type: .Bold))
}
} else {
}
}
}
@ -524,17 +528,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
ribbonTitle = item.presentationData.strings.Notification_StarGift_OneOf(availabilityString).string
}
if incoming, let upgradeStars, upgradeStars > 0, !upgraded {
if incoming || item.presentationData.isPreview, let upgradeStars, upgradeStars > 0, !upgraded {
buttonTitle = item.presentationData.strings.Notification_StarGift_Unpack
buttonIcon = "Premium/GiftUnpack"
} else {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
}
}
case let .starGiftUnique(gift, _, _, _, _, _, isRefunded):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded):
if case let .unique(uniqueGift) = gift {
isStarGift = true
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let authorName: String
if isUpgrade && item.message.author?.id == item.context.account.peerId {
authorName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
} else {
authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
}
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
text = "**\(uniqueGift.title) #\(uniqueGift.number)**"
ribbonTitle = item.presentationData.strings.Notification_StarGift_Gift

View file

@ -763,7 +763,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
})
}
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: {
let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: {
if let strongSelf = self {
if let item = strongSelf.item {
if strongSelf.infoBackgroundNode.alpha.isZero {

View file

@ -1830,7 +1830,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
)
}
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in
if let strongSelf = self {

View file

@ -222,7 +222,7 @@ public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleCont
let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
if videoContent.id != strongSelf.videoContent?.id {
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in
if let strongSelf = self {

Some files were not shown because too many files have changed in this diff Show more