mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'master' into beta
# Conflicts: # Telegram/Telegram-iOS/en.lproj/Localizable.strings # submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift # versions.json
This commit is contained in:
commit
8b6389e2a8
11086 changed files with 1301189 additions and 830765 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -29,3 +29,6 @@ url=../tgcalls.git
|
|||
[submodule "submodules/LottieCpp/lottiecpp"]
|
||||
path = submodules/LottieCpp/lottiecpp
|
||||
url = https://github.com/ali-fareed/lottiecpp.git
|
||||
[submodule "third-party/dav1d/dav1d"]
|
||||
path = third-party/dav1d/dav1d
|
||||
url = https://github.com/ali-fareed/dav1d.git
|
||||
|
|
|
|||
|
|
@ -1746,6 +1746,7 @@ ios_extension(
|
|||
":SwiftSignalKitFramework",
|
||||
":PostboxFramework",
|
||||
":TelegramCoreFramework",
|
||||
":TelegramUIFramework",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ swift_library(
|
|||
"//submodules/GZip:GZip",
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
"//submodules/Utils/RangeSet",
|
||||
"//submodules/Media/ConvertOpusToAAC",
|
||||
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import CallKit
|
|||
import AppLockState
|
||||
import NotificationsPresentationData
|
||||
import RangeSet
|
||||
import ConvertOpusToAAC
|
||||
|
||||
private let queue = Queue()
|
||||
|
||||
|
|
@ -440,11 +441,11 @@ private func avatarImage(path: String?, peerId: PeerId, letters: [String], size:
|
|||
}
|
||||
}
|
||||
|
||||
private func storeTemporaryImage(path: String) -> String {
|
||||
private func storeTemporaryImage(path: String, fileExtension: String) -> String {
|
||||
let imagesPath = NSTemporaryDirectory() + "/aps-data"
|
||||
let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: imagesPath), withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let tempPath = imagesPath + "\(path.persistentHashValue)"
|
||||
let tempPath = imagesPath + "\(path.persistentHashValue).\(fileExtension)"
|
||||
if FileManager.default.fileExists(atPath: tempPath) {
|
||||
return tempPath
|
||||
}
|
||||
|
|
@ -459,7 +460,7 @@ private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, i
|
|||
if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let path = mediaBox.completedResourcePath(resource) {
|
||||
let cachedPath = mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "intents\(isStory ? "-story2" : "").png", keepDuration: .shortLived)
|
||||
if let _ = fileSize(cachedPath), !"".isEmpty {
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath, fileExtension: "jpg")))
|
||||
} else {
|
||||
let image = avatarImage(path: path, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0), isStory: isStory)
|
||||
if let data = image.pngData() {
|
||||
|
|
@ -467,19 +468,19 @@ private func peerAvatar(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, i
|
|||
let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
|
||||
}
|
||||
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath, fileExtension: "jpg")))
|
||||
}
|
||||
}
|
||||
|
||||
let cachedPath = mediaBox.cachedRepresentationPathForId("lettersAvatar2-\(peer.displayLetters.joined(separator: ","))\(isStory ? "-story" : "")", representationId: "intents.png", keepDuration: .shortLived)
|
||||
if let _ = fileSize(cachedPath) {
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath, fileExtension: "jpg")))
|
||||
} else {
|
||||
let image = avatarImage(path: nil, peerId: peer.id, letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0), isStory: isStory)
|
||||
if let data = image.pngData() {
|
||||
let _ = try? data.write(to: URL(fileURLWithPath: cachedPath), options: .atomic)
|
||||
}
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath)))
|
||||
return INImage(url: URL(fileURLWithPath: storeTemporaryImage(path: cachedPath, fileExtension: "jpg")))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1247,7 +1248,9 @@ private final class NotificationServiceHandler {
|
|||
case let .poll(peerId, initialContent, messageId):
|
||||
Logger.shared.log("NotificationService \(episode)", "Will poll")
|
||||
if let stateManager = strongSelf.stateManager {
|
||||
let pollCompletion: (NotificationContent) -> Void = { content in
|
||||
let shouldKeepConnection = stateManager.network.shouldKeepConnection
|
||||
|
||||
let pollCompletion: (NotificationContent, Media?) -> Void = { content, customMedia in
|
||||
var content = content
|
||||
|
||||
queue.async {
|
||||
|
|
@ -1257,6 +1260,8 @@ private final class NotificationServiceHandler {
|
|||
completed()
|
||||
return
|
||||
}
|
||||
|
||||
let mediaAttachment = mediaAttachment ?? customMedia
|
||||
|
||||
var fetchMediaSignal: Signal<Data?, NoError> = .single(nil)
|
||||
if let mediaAttachment = mediaAttachment {
|
||||
|
|
@ -1272,6 +1277,9 @@ private final class NotificationServiceHandler {
|
|||
} else if file.isVideo {
|
||||
fetchResource = file.previewRepresentations.first?.resource as? TelegramMultipartFetchableResource
|
||||
contentType = .video
|
||||
} else if file.isVoice {
|
||||
fetchResource = file.resource as? TelegramMultipartFetchableResource
|
||||
contentType = .audio
|
||||
} else {
|
||||
contentType = .file
|
||||
}
|
||||
|
|
@ -1443,8 +1451,10 @@ private final class NotificationServiceHandler {
|
|||
completed()
|
||||
return
|
||||
}
|
||||
|
||||
shouldKeepConnection.set(.single(false))
|
||||
|
||||
Logger.shared.log("NotificationService \(episode)", "Did fetch media \(mediaData == nil ? "Non-empty" : "Empty")")
|
||||
Logger.shared.log("NotificationService \(episode)", "Did fetch media \(mediaData == nil ? "Empty" : "Non-empty")")
|
||||
|
||||
if let notificationSoundData = notificationSoundData {
|
||||
Logger.shared.log("NotificationService \(episode)", "Did fetch notificationSoundData")
|
||||
|
|
@ -1527,6 +1537,24 @@ private final class NotificationServiceHandler {
|
|||
content.attachments.append(attachment)
|
||||
}
|
||||
}
|
||||
} else if file.isVoice {
|
||||
if let mediaData = mediaData {
|
||||
stateManager.postbox.mediaBox.storeResourceData(file.resource.id, data: mediaData, synchronous: true)
|
||||
}
|
||||
if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(file.resource, pathExtension: nil) {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "audio.m4a")
|
||||
let _ = (convertOpusToAAC(sourcePath: storedPath, allocateTempFile: {
|
||||
return tempFile.path
|
||||
})
|
||||
|> timeout(5.0, queue: .concurrentDefaultQueue(), alternate: .single(nil))).startStandalone(next: { _ in
|
||||
semaphore.signal()
|
||||
})
|
||||
semaphore.wait()
|
||||
if let attachment = try? UNNotificationAttachment(identifier: "audio", url: URL(fileURLWithPath: tempFile.path), options: nil) {
|
||||
content.attachments.append(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1550,11 +1578,10 @@ private final class NotificationServiceHandler {
|
|||
}
|
||||
|
||||
let pollSignal: Signal<Never, NoError>
|
||||
|
||||
|
||||
if !shouldSynchronizeState {
|
||||
pollSignal = .complete()
|
||||
} else {
|
||||
let shouldKeepConnection = stateManager.network.shouldKeepConnection
|
||||
shouldKeepConnection.set(.single(true))
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
Logger.shared.log("NotificationService \(episode)", "Will poll channel \(peerId)")
|
||||
|
|
@ -1566,9 +1593,6 @@ private final class NotificationServiceHandler {
|
|||
peerId: peerId,
|
||||
stateManager: stateManager
|
||||
)
|
||||
|> afterDisposed { [weak shouldKeepConnection] in
|
||||
shouldKeepConnection?.set(.single(false))
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("NotificationService \(episode)", "Will perform non-specific getDifference")
|
||||
enum ControlError {
|
||||
|
|
@ -1584,17 +1608,14 @@ private final class NotificationServiceHandler {
|
|||
}
|
||||
}
|
||||
|> restartIfError
|
||||
|> afterDisposed { [weak shouldKeepConnection] in
|
||||
shouldKeepConnection?.set(.single(false))
|
||||
}
|
||||
|
||||
pollSignal = signal
|
||||
}
|
||||
}
|
||||
|
||||
let pollWithUpdatedContent: Signal<NotificationContent, NoError>
|
||||
let pollWithUpdatedContent: Signal<(NotificationContent, Media?), NoError>
|
||||
if interactionAuthorId != nil || messageId != nil {
|
||||
pollWithUpdatedContent = stateManager.postbox.transaction { transaction -> NotificationContent in
|
||||
pollWithUpdatedContent = stateManager.postbox.transaction { transaction -> (NotificationContent, Media?) in
|
||||
var content = initialContent
|
||||
|
||||
if let interactionAuthorId = interactionAuthorId {
|
||||
|
|
@ -1639,22 +1660,37 @@ private final class NotificationServiceHandler {
|
|||
}
|
||||
}
|
||||
|
||||
return content
|
||||
return (content, nil)
|
||||
}
|
||||
|> then(
|
||||
pollSignal
|
||||
|> map { _ -> NotificationContent in }
|
||||
|> map { _ -> (NotificationContent, Media?) in }
|
||||
)
|
||||
|> takeLast
|
||||
|> mapToSignal { content, _ -> Signal<(NotificationContent, Media?), NoError> in
|
||||
return stateManager.postbox.transaction { transaction -> (NotificationContent, Media?) in
|
||||
var parsedMedia: Media?
|
||||
if let messageId, let message = transaction.getMessage(messageId) {
|
||||
if let media = message.media.first {
|
||||
parsedMedia = media
|
||||
}
|
||||
}
|
||||
|
||||
return (content, parsedMedia)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pollWithUpdatedContent = pollSignal
|
||||
|> map { _ -> NotificationContent in }
|
||||
|> map { _ -> (NotificationContent, Media?) in }
|
||||
}
|
||||
|
||||
var updatedContent = initialContent
|
||||
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content in
|
||||
var updatedMedia: Media?
|
||||
strongSelf.pollDisposable.set(pollWithUpdatedContent.start(next: { content, media in
|
||||
updatedContent = content
|
||||
updatedMedia = media
|
||||
}, completed: {
|
||||
pollCompletion(updatedContent)
|
||||
pollCompletion(updatedContent, updatedMedia)
|
||||
}))
|
||||
} else {
|
||||
completed()
|
||||
|
|
|
|||
|
|
@ -13297,3 +13297,146 @@ Sorry for the inconvenience.";
|
|||
"WebApp.ShareMessage.PreviewTitle" = "MESSAGE PREVIEW";
|
||||
"WebApp.ShareMessage.Info" = "%@ mini app suggests you to send this message to a chat you select.";
|
||||
"WebApp.ShareMessage.Share" = "Share With...";
|
||||
|
||||
"Notification.Gift" = "Gift";
|
||||
|
||||
"WebBrowser.PassExistsError" = "This pass is already added to Wallet.";
|
||||
|
||||
"Chat.VideoProcessingInfo" = "The video will be published once converted and optimized.";
|
||||
|
||||
"Camera.CollageManagementTooltip" = "Tap a tile to delete or reorder it.";
|
||||
"Camera.CollageReorderingInfo" = "Hold and drag tiles to reorder them.";
|
||||
|
||||
"MediaPicker.InvertCaptionTooltip" = "Tap here to move caption up.";
|
||||
"MediaPicker.InvertCaption.Updated.Up.Title" = "Caption moved up";
|
||||
"MediaPicker.InvertCaption.Updated.Up.Text" = "Text will be shown above the media.";
|
||||
"MediaPicker.InvertCaption.Updated.Down.Title" = "Caption moved down";
|
||||
"MediaPicker.InvertCaption.Updated.Down.Text" = "Text will be shown below the media.";
|
||||
|
||||
"AffiliateSetup.AlertTerminate.Title" = "Warning";
|
||||
"AffiliateSetup.AlertTerminate.Text" = "If you end your affiliate program:\n\n• Any referral links already shared will be disabled in 24 hours.\n\n• All participating affiliates will be notified.\n\n• You will be able to start a new affiliate program only in 24 hours.";
|
||||
"AffiliateSetup.AlertTerminate.Action" = "End Anyway";
|
||||
"AffiliateSetup.AlertApply.Title" = "Warning";
|
||||
"AffiliateSetup.AlertApply.Text" = "Once you start the affiliate program, you won't be able to decrease its commission or duration. You can only increase these parameters or end the program, whuch will disable all previously distributed referral links.";
|
||||
"AffiliateSetup.AlertApply.SectionCommission" = "Commission";
|
||||
"AffiliateSetup.AlertApply.SectionDuration" = "Duration";
|
||||
"AffiliateSetup.AlertApply.Action" = "Start";
|
||||
|
||||
"AffiliateSetup.ToastTerminated.Title" = "Affiliate Program Ended";
|
||||
"AffiliateSetup.ToastTerminated.Text" = "Participating affiliates have been notified. All referral links will be disabled in 24 hours.";
|
||||
|
||||
"AffiliateSetup.SectionDuration" = "DURATION";
|
||||
"AffiliateSetup.SectionDurationFooter" = "Set the duration for which affiliates will earn commissions from referred users.";
|
||||
|
||||
"AffiliateSetup.SectionCommission" = "COMMISSION";
|
||||
"AffiliateSetup.SectionCommissionFooter" = "Define the percentage of star revenue your affiliates earn for referring users to your bot.";
|
||||
|
||||
"AffiliateSetup.ExistingPrograms.Action" = "View Existing Programs";
|
||||
"AffiliateSetup.ExistingPrograms.Footer" = "Explore what other mini apps offer.";
|
||||
|
||||
"AffiliateSetup.EndAction" = "End Affiliate Program";
|
||||
|
||||
"AffiliateSetup.StartTitle" = "Start Affiliate Program";
|
||||
"AffiliateSetup.UpdateTitle" = "Update Affiliate Program";
|
||||
|
||||
"AffiliateSetup.TermsFooter" = "By creating an affiliate program, you afree to the [terms and conditions](https://telegram.org/terms) of Affiliate Programs.";
|
||||
|
||||
"AffiliateSetup.ProgramMenu.OpenBot" = "Open Bot";
|
||||
"AffiliateSetup.ProgramMenu.OpenApp" = "Open App";
|
||||
"AffiliateSetup.ProgramMenu.CopyLink" = "Copy Link";
|
||||
"AffiliateSetup.ProgramMenu.Leave" = "Leave";
|
||||
"AffiliateSetup.ProgramLeave" = "Leave";
|
||||
|
||||
"AffiliateSetup.ConnectedSectionTitle" = "MY PROGRAMS";
|
||||
"AffiliateSetup.SuggestedSectionTitle" = "PROGRAMS";
|
||||
"AffiliateSetup.SuggestedSectionEmpty" = "No available programs yet.\nPlease check the page later.";
|
||||
|
||||
"AffiliateSetup.TitleNew" = "Affiliate Program";
|
||||
"AffiliateSetup.TextNew" = "Affiliate Program";
|
||||
"AffiliateSetup.TitleJoin" = "Affiliate Programs";
|
||||
"AffiliateSetup.TextJoin" = "Earn a commission each time a user who first accessed a mini app through your referral link spends **Stars** within it.";
|
||||
|
||||
"AffiliateSetup.IntroNew.Title1" = "Share revenue with affiliates";
|
||||
"AffiliateSetup.IntroNew.Text1" = "Set the commission for revenue generated by users referred to you.";
|
||||
"AffiliateSetup.IntroNew.Title2" = "Launch your affiliate program";
|
||||
"AffiliateSetup.IntroNew.Text2" = "Telegram will feature your program for millions of potential affiliates.";
|
||||
"AffiliateSetup.IntroNew.Title3" = "Let affiliates promote you";
|
||||
"AffiliateSetup.IntroNew.Text3" = "Affiliates will share your referral link with their audience.";
|
||||
|
||||
"AffiliateSetup.IntroJoin.Title1" = "Reliable";
|
||||
"AffiliateSetup.IntroJoin.Text1" = "Receive guaranteed commissions for spending by users you refer.";
|
||||
"AffiliateSetup.IntroJoin.Title2" = "Transparent";
|
||||
"AffiliateSetup.IntroJoin.Text2" = "Track your commissions from referred users in real time.";
|
||||
"AffiliateSetup.IntroJoin.Title3" = "Simple";
|
||||
"AffiliateSetup.IntroJoin.Text3" = "Choose a mini app below, get your referral link, and start earning Stars.";
|
||||
|
||||
"AffiliateProgram.ToastLinkCopied.Title" = "Link copied to clipboard";
|
||||
"AffiliateProgram.ToastLinkCopied.Text" = "Share this link and earn **%1$@** of what people who use it spend in **%2$@!";
|
||||
|
||||
"AffiliateProgram.DurationLifetime" = "Lifetime";
|
||||
|
||||
"AffiliateProgram.SortSelectorProfitability" = "Profitability";
|
||||
"AffiliateProgram.SortSelectorRevenue" = "Revenue";
|
||||
"AffiliateProgram.SortSelectorDate" = "Date";
|
||||
|
||||
"AffiliateProgram.ValueShortMonths_1" = "%@m";
|
||||
"AffiliateProgram.ValueShortMonths_any" = "%@m";
|
||||
"AffiliateProgram.ValueShortYears_1" = "%@y";
|
||||
"AffiliateProgram.ValueShortYears_any" = "%@y";
|
||||
|
||||
"AffiliateProgram.ValueLongMonths_1" = "%@ MONTH";
|
||||
"AffiliateProgram.ValueLongMonths_any" = "%@ MONTHS";
|
||||
"AffiliateProgram.ValueLongYears_1" = "%@ YEAR";
|
||||
"AffiliateProgram.ValueLongYears_any" = "%@ YEARS";
|
||||
|
||||
"AffiliateProgram.PeerTypeSelf" = "personal account";
|
||||
|
||||
"AffiliateProgram.JoinTitle" = "Affiliate Program";
|
||||
"AffiliateProgram.JoinTerms" = "By joining this program, you afree to the [terms and conditions](https://telegram.org/terms) of Affiliate Programs.";
|
||||
"AffiliateProgram.DailyRevenueText" = "The average daily revenue per user: #**%@**";
|
||||
|
||||
"AffiliateProgram.LinkTitle" = "Referral Link";
|
||||
"AffiliateProgram.LinkSubtitleLifetime" = "Share this link with your users to earn a **{commission}** commission on their spending in **{bot}** **forever** after they follow your link.";
|
||||
"AffiliateProgram.LinkSubtitleMonths_1" = "Share this link with your users to earn a **{commission}** commission on their spending in **{bot}** for **1 month** after they follow your link.";
|
||||
"AffiliateProgram.LinkSubtitleMonths_any" = "Share this link with your users to earn a **{commission}** commission on their spending in **{bot}** for **%d months** after they follow your link.";
|
||||
"AffiliateProgram.LinkSubtitleYears_1" = "Share this link with your users to earn a **{commission}** commission on their spending in **{bot}** for **1 year** after they follow your link.";
|
||||
"AffiliateProgram.LinkSubtitleYears_any" = "Share this link with your users to earn a **{commission}** commission on their spending in **{bot}** for **%d years** after they follow your link.";
|
||||
|
||||
"AffiliateProgram.JoinSubtitleLifetime" = "**{bot}** will share **{commission}** of the revenue from each user you refer to it **forever**.";
|
||||
"AffiliateProgram.JoinSubtitleMonths_1" = "**{bot}** will share **{commission}** of the revenue from each user you refer to it for **1 month.**";
|
||||
"AffiliateProgram.JoinSubtitleMonths_any" = "**{bot}** will share **{commission}** of the revenue from each user you refer to it for **%d months.**";
|
||||
"AffiliateProgram.JoinSubtitleYears_1" = "**{bot}** will share **{commission}** of the revenue from each user you refer to it for **1 month.**";
|
||||
"AffiliateProgram.JoinSubtitleYears_any" = "**{bot}** will share **{commission}** of the revenue from each user you refer to it for **%d months.**";
|
||||
|
||||
"AffiliateProgram.CommistionDestinationText" = "Commission will be sent to:";
|
||||
|
||||
"AffiliateProgram.ActionJoin" = "Join Program";
|
||||
"AffiliateProgram.ActionCopyLink" = "Copy Link";
|
||||
|
||||
"AffiliateProgram.ToastJoined.Title" = "Program joined";
|
||||
"AffiliateProgram.ToastJoined.Text" = "You can now copy the referral link.";
|
||||
|
||||
"AffiliateSetup.SortSectionHeader.Date" = "SORT BY [DATE]()";
|
||||
"AffiliateSetup.SortSectionHeader.Profitability" = "SORT BY [PROFITABILITY]()";
|
||||
"AffiliateSetup.SortSectionHeader.Revenue" = "SORT BY [REVENUE]()";
|
||||
|
||||
"AffiliateProgram.UserCountFooter_0" = "No one opened {bot} through this link yet.";
|
||||
"AffiliateProgram.UserCountFooter_1" = "1 user opened {bot} through this link.";
|
||||
"AffiliateProgram.UserCountFooter_any" = "%d users opened {bot} through this link.";
|
||||
|
||||
"ChatList.InlineButtonOpenApp" = "OPEN";
|
||||
|
||||
"Monetization.EarnStarsInfo.Title" = "Earn Stars";
|
||||
"Monetization.EarnStarsInfo.Text" = "Distribute links to mini apps and earn a share of their revenue in Stars.";
|
||||
|
||||
"PeerInfo.ItemAffiliateProgram.Title" = "Affiliate Program";
|
||||
"PeerInfo.ItemAffiliatePrograms.Title" = "Affiliate Programs";
|
||||
"PeerInfo.ItemAffiliateProgram.Footer" = "Share a link to %1$@ with your friends and and earn %2$@% of their spending there.";
|
||||
"PeerInfo.ItemAffiliateProgram.ValueOff" = "Off";
|
||||
|
||||
"StarsTransaction.TitleCommission" = "%@% Commission";
|
||||
"StarsTransaction.StarRefReason.Title" = "Reason";
|
||||
"StarsTransaction.StarRefReason.Program" = "Affiliate Program";
|
||||
"StarsTransaction.StarRefReason.Miniapp" = "Mini App";
|
||||
"StarsTransaction.StarRefReason.Affiliate" = "Affiliate";
|
||||
"StarsTransaction.StarRefReason.Referred" = "Referred User";
|
||||
|
|
|
|||
12
WORKSPACE
12
WORKSPACE
|
|
@ -33,6 +33,18 @@ http_file(
|
|||
sha256 = "f794ed92ccb4e9b6619a77328f313497d7decf8fb7e047ba35a348b838e0e1e2",
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "meson_tar_gz",
|
||||
urls = ["https://github.com/mesonbuild/meson/releases/download/1.6.0/meson-1.6.0.tar.gz"],
|
||||
sha256 = "999b65f21c03541cf11365489c1fad22e2418bb0c3d50ca61139f2eec09d5496",
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "ninja-mac_zip",
|
||||
urls = ["https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-mac.zip"],
|
||||
sha256 = "89a287444b5b3e98f88a945afa50ce937b8ffd1dcc59c555ad9b1baf855298c9",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "appcenter_sdk",
|
||||
urls = ["https://github.com/microsoft/appcenter-sdk-apple/releases/download/4.1.1/AppCenter-SDK-Apple-4.1.1.zip"],
|
||||
|
|
|
|||
|
|
@ -777,6 +777,17 @@ public class MediaEditorTransitionOutExternalState {
|
|||
}
|
||||
}
|
||||
|
||||
public protocol CameraScreen: ViewController {
|
||||
|
||||
}
|
||||
|
||||
public protocol MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
public protocol MediaPickerScreen: ViewController {
|
||||
func dismissAnimated()
|
||||
}
|
||||
|
||||
public protocol MediaEditorScreenResult {
|
||||
var target: Stories.PendingTarget { get }
|
||||
}
|
||||
|
|
@ -812,6 +823,14 @@ public protocol CollectibleItemInfoScreenInitialData: AnyObject {
|
|||
public protocol BusinessLinksSetupScreenInitialData: AnyObject {
|
||||
}
|
||||
|
||||
public enum AffiliateProgramSetupScreenMode {
|
||||
case editProgram
|
||||
case connectedPrograms
|
||||
}
|
||||
|
||||
public protocol AffiliateProgramSetupScreenInitialData: AnyObject {
|
||||
}
|
||||
|
||||
public enum CollectibleItemInfoScreenSubject {
|
||||
case phoneNumber(String)
|
||||
case username(String)
|
||||
|
|
@ -862,6 +881,36 @@ public final class BotPreviewEditorTransitionOut {
|
|||
public protocol MiniAppListScreenInitialData: AnyObject {
|
||||
}
|
||||
|
||||
public enum JoinAffiliateProgramScreenMode {
|
||||
public final class Join {
|
||||
public let initialTargetPeer: EnginePeer
|
||||
public let canSelectTargetPeer: Bool
|
||||
public let completion: (EnginePeer) -> Void
|
||||
|
||||
public init(initialTargetPeer: EnginePeer, canSelectTargetPeer: Bool, completion: @escaping (EnginePeer) -> Void) {
|
||||
self.initialTargetPeer = initialTargetPeer
|
||||
self.canSelectTargetPeer = canSelectTargetPeer
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
public final class Active {
|
||||
public let targetPeer: EnginePeer
|
||||
public let bot: TelegramConnectedStarRefBotList.Item
|
||||
public let copyLink: (TelegramConnectedStarRefBotList.Item) -> Void
|
||||
|
||||
public init(targetPeer: EnginePeer, bot: TelegramConnectedStarRefBotList.Item, copyLink: @escaping (TelegramConnectedStarRefBotList.Item) -> Void) {
|
||||
self.targetPeer = targetPeer
|
||||
self.bot = bot
|
||||
self.copyLink = copyLink
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case join(Join)
|
||||
case active(Active)
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
|
|
@ -1015,7 +1064,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||
func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
|
||||
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
|
||||
|
||||
|
|
@ -1052,6 +1101,11 @@ public protocol SharedAccountContext: AnyObject {
|
|||
|
||||
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
|
||||
|
||||
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
|
||||
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController
|
||||
|
||||
func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
||||
func navigateToCurrentCall()
|
||||
|
|
|
|||
|
|
@ -504,6 +504,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
public var focusUpdated: ((Bool) -> Void)?
|
||||
public var heightUpdated: ((Bool) -> Void)?
|
||||
public var timerUpdated: ((NSNumber?) -> Void)?
|
||||
public var captionIsAboveUpdated: ((Bool) -> Void)?
|
||||
|
||||
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
|
||||
guard let presentationInterfaceState = self.presentationInterfaceState else {
|
||||
|
|
@ -518,7 +519,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||
}
|
||||
}
|
||||
|
||||
public func setTimeout(_ timeout: Int32, isVideo: Bool) {
|
||||
public func setTimeout(_ timeout: Int32, isVideo: Bool, isCaptionAbove: Bool) {
|
||||
}
|
||||
|
||||
public func animate(_ view: UIView, frame: CGRect) {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ private class AvatarNodeParameters: NSObject {
|
|||
let explicitColorIndex: Int?
|
||||
let hasImage: Bool
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
let cutoutRect: CGRect?
|
||||
|
||||
init(theme: PresentationTheme?, accountPeerId: EnginePeer.Id?, peerId: EnginePeer.Id?, colors: [UIColor], letters: [String], font: UIFont, icon: AvatarNodeIcon, explicitColorIndex: Int?, hasImage: Bool, clipStyle: AvatarNodeClipStyle) {
|
||||
init(theme: PresentationTheme?, accountPeerId: EnginePeer.Id?, peerId: EnginePeer.Id?, colors: [UIColor], letters: [String], font: UIFont, icon: AvatarNodeIcon, explicitColorIndex: Int?, hasImage: Bool, clipStyle: AvatarNodeClipStyle, cutoutRect: CGRect?) {
|
||||
self.theme = theme
|
||||
self.accountPeerId = accountPeerId
|
||||
self.peerId = peerId
|
||||
|
|
@ -58,12 +59,13 @@ private class AvatarNodeParameters: NSObject {
|
|||
self.explicitColorIndex = explicitColorIndex
|
||||
self.hasImage = hasImage
|
||||
self.clipStyle = clipStyle
|
||||
self.cutoutRect = cutoutRect
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func withUpdatedHasImage(_ hasImage: Bool) -> AvatarNodeParameters {
|
||||
return AvatarNodeParameters(theme: self.theme, accountPeerId: self.accountPeerId, peerId: self.peerId, colors: self.colors, letters: self.letters, font: self.font, icon: self.icon, explicitColorIndex: self.explicitColorIndex, hasImage: hasImage, clipStyle: self.clipStyle)
|
||||
return AvatarNodeParameters(theme: self.theme, accountPeerId: self.accountPeerId, peerId: self.peerId, colors: self.colors, letters: self.letters, font: self.font, icon: self.icon, explicitColorIndex: self.explicitColorIndex, hasImage: hasImage, clipStyle: self.clipStyle, cutoutRect: self.cutoutRect)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +168,7 @@ public enum AvatarNodeExplicitIcon {
|
|||
|
||||
private enum AvatarNodeState: Equatable {
|
||||
case empty
|
||||
case peerAvatar(EnginePeer.Id, PeerNameColor?, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle)
|
||||
case peerAvatar(EnginePeer.Id, PeerNameColor?, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle, CGRect?)
|
||||
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
|
||||
}
|
||||
|
||||
|
|
@ -174,8 +176,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
|||
switch (lhs, rhs) {
|
||||
case (.empty, .empty):
|
||||
return true
|
||||
case let (.peerAvatar(lhsPeerId, lhsPeerNameColor, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsPeerNameColor, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)):
|
||||
return lhsPeerId == rhsPeerId && lhsPeerNameColor == rhsPeerNameColor && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle
|
||||
case let (.peerAvatar(lhsPeerId, lhsPeerNameColor, lhsLetters, lhsPhotoRepresentations, lhsClipStyle, lhsCutoutRect), .peerAvatar(rhsPeerId, rhsPeerNameColor, rhsLetters, rhsPhotoRepresentations, rhsClipStyle, rhsCutoutRect)):
|
||||
return lhsPeerId == rhsPeerId && lhsPeerNameColor == rhsPeerNameColor && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle && lhsCutoutRect == rhsCutoutRect
|
||||
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
|
||||
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
|
||||
default:
|
||||
|
|
@ -307,7 +309,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
didSet {
|
||||
if oldValue.pointSize != font.pointSize {
|
||||
if let parameters = self.parameters {
|
||||
self.parameters = AvatarNodeParameters(theme: parameters.theme, accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, colors: parameters.colors, letters: parameters.letters, font: self.font, icon: parameters.icon, explicitColorIndex: parameters.explicitColorIndex, hasImage: parameters.hasImage, clipStyle: parameters.clipStyle)
|
||||
self.parameters = AvatarNodeParameters(theme: parameters.theme, accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, colors: parameters.colors, letters: parameters.letters, font: self.font, icon: parameters.icon, explicitColorIndex: parameters.explicitColorIndex, hasImage: parameters.hasImage, clipStyle: parameters.clipStyle, cutoutRect: parameters.cutoutRect)
|
||||
}
|
||||
|
||||
if !self.displaySuspended {
|
||||
|
|
@ -334,7 +336,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
var clipStyle: AvatarNodeClipStyle {
|
||||
if let params = self.params {
|
||||
return params.clipStyle
|
||||
} else if case let .peerAvatar(_, _, _, _, clipStyle) = self.state {
|
||||
} else if case let .peerAvatar(_, _, _, _, clipStyle, _) = self.state {
|
||||
return clipStyle
|
||||
}
|
||||
return .none
|
||||
|
|
@ -498,7 +500,8 @@ public final class AvatarNode: ASDisplayNode {
|
|||
clipStyle: AvatarNodeClipStyle = .round,
|
||||
synchronousLoad: Bool = false,
|
||||
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
||||
storeUnrounded: Bool = false
|
||||
storeUnrounded: Bool = false,
|
||||
cutoutRect: CGRect? = nil
|
||||
) {
|
||||
var synchronousLoad = synchronousLoad
|
||||
var representation: TelegramMediaImageRepresentation?
|
||||
|
|
@ -542,7 +545,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
representation = peer?.smallProfileImage
|
||||
}
|
||||
|
||||
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle)
|
||||
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle, cutoutRect)
|
||||
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
|
||||
self.state = updatedState
|
||||
self.overrideImage = overrideImage
|
||||
|
|
@ -550,7 +553,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
|
||||
let parameters: AvatarNodeParameters
|
||||
|
||||
if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) {
|
||||
if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) {
|
||||
self.contents = nil
|
||||
self.displaySuspended = true
|
||||
self.imageReady.set(self.imageNode.contentReady)
|
||||
|
|
@ -577,7 +580,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
self.editOverlayNode?.isHidden = true
|
||||
}
|
||||
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle, cutoutRect: cutoutRect)
|
||||
} else {
|
||||
self.imageReady.set(.single(true))
|
||||
self.displaySuspended = false
|
||||
|
|
@ -587,7 +590,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
|
||||
self.editOverlayNode?.isHidden = true
|
||||
let colors = calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle, cutoutRect: cutoutRect)
|
||||
|
||||
if let badgeView = self.badgeView {
|
||||
let badgeColor: UIColor
|
||||
|
|
@ -673,7 +676,8 @@ public final class AvatarNode: ASDisplayNode {
|
|||
clipStyle: AvatarNodeClipStyle = .round,
|
||||
synchronousLoad: Bool = false,
|
||||
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
||||
storeUnrounded: Bool = false
|
||||
storeUnrounded: Bool = false,
|
||||
cutoutRect: CGRect? = nil
|
||||
) {
|
||||
var synchronousLoad = synchronousLoad
|
||||
var representation: TelegramMediaImageRepresentation?
|
||||
|
|
@ -717,7 +721,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
representation = peer?.smallProfileImage
|
||||
}
|
||||
|
||||
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle)
|
||||
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle, cutoutRect)
|
||||
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
|
||||
self.state = updatedState
|
||||
self.overrideImage = overrideImage
|
||||
|
|
@ -727,7 +731,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
|
||||
let account = account ?? genericContext.account
|
||||
|
||||
if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) {
|
||||
if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) {
|
||||
self.contents = nil
|
||||
self.displaySuspended = true
|
||||
self.imageReady.set(self.imageNode.contentReady)
|
||||
|
|
@ -754,7 +758,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
self.editOverlayNode?.isHidden = true
|
||||
}
|
||||
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle, cutoutRect: cutoutRect)
|
||||
} else {
|
||||
self.imageReady.set(.single(true))
|
||||
self.displaySuspended = false
|
||||
|
|
@ -764,7 +768,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
|
||||
self.editOverlayNode?.isHidden = true
|
||||
let colors = calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
|
||||
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle, cutoutRect: cutoutRect)
|
||||
|
||||
if let badgeView = self.badgeView {
|
||||
let badgeColor: UIColor
|
||||
|
|
@ -786,7 +790,7 @@ public final class AvatarNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
|
||||
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil, cutoutRect: CGRect? = nil) {
|
||||
var explicitIndex: Int?
|
||||
if let explicitColor = explicitColor {
|
||||
switch explicitColor {
|
||||
|
|
@ -800,9 +804,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||
|
||||
let parameters: AvatarNodeParameters
|
||||
if let icon = icon, case .phone = icon {
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round, cutoutRect: cutoutRect)
|
||||
} else {
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
|
||||
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round, cutoutRect: cutoutRect)
|
||||
}
|
||||
|
||||
self.displaySuspended = true
|
||||
|
|
@ -998,6 +1002,12 @@ public final class AvatarNode: ASDisplayNode {
|
|||
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
|
||||
}
|
||||
}
|
||||
|
||||
if let parameters = parameters as? AvatarNodeParameters, let cutoutRect = parameters.cutoutRect {
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: bounds.height - cutoutRect.maxY - cutoutRect.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1190,7 +1200,8 @@ public final class AvatarNode: ASDisplayNode {
|
|||
clipStyle: AvatarNodeClipStyle = .round,
|
||||
synchronousLoad: Bool = false,
|
||||
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
|
||||
storeUnrounded: Bool = false
|
||||
storeUnrounded: Bool = false,
|
||||
cutoutRect: CGRect? = nil
|
||||
) {
|
||||
self.contentNode.setPeer(
|
||||
context: context,
|
||||
|
|
@ -1203,7 +1214,8 @@ public final class AvatarNode: ASDisplayNode {
|
|||
clipStyle: clipStyle,
|
||||
synchronousLoad: synchronousLoad,
|
||||
displayDimensions: displayDimensions,
|
||||
storeUnrounded: storeUnrounded
|
||||
storeUnrounded: storeUnrounded,
|
||||
cutoutRect: cutoutRect
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ public func peerAvatarCompleteImage(postbox: Postbox, network: Network, peer: En
|
|||
return iconSignal
|
||||
}
|
||||
|
||||
public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), clipStyle: AvatarNodeClipStyle = .round, blurred: Bool = false, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false) -> Signal<(UIImage, UIImage)?, NoError>? {
|
||||
public func peerAvatarImage(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), clipStyle: AvatarNodeClipStyle = .round, blurred: Bool = false, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false, cutoutRect: CGRect? = nil) -> Signal<(UIImage, UIImage)?, NoError>? {
|
||||
return peerAvatarImage(
|
||||
postbox: account.postbox,
|
||||
network: account.network,
|
||||
|
|
@ -184,11 +184,12 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut
|
|||
inset: inset,
|
||||
emptyColor: emptyColor,
|
||||
synchronousLoad: synchronousLoad,
|
||||
provideUnrounded: synchronousLoad
|
||||
provideUnrounded: synchronousLoad,
|
||||
cutoutRect: cutoutRect
|
||||
)
|
||||
}
|
||||
|
||||
public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), clipStyle: AvatarNodeClipStyle = .round, blurred: Bool = false, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false) -> Signal<(UIImage, UIImage)?, NoError>? {
|
||||
public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), clipStyle: AvatarNodeClipStyle = .round, blurred: Bool = false, inset: CGFloat = 0.0, emptyColor: UIColor? = nil, synchronousLoad: Bool = false, provideUnrounded: Bool = false, cutoutRect: CGRect? = nil) -> Signal<(UIImage, UIImage)?, NoError>? {
|
||||
if let imageData = peerAvatarImageData(postbox: postbox, network: network, peerReference: peerReference, authorOfMessage: authorOfMessage, representation: representation, synchronousLoad: synchronousLoad) {
|
||||
return imageData
|
||||
|> mapToSignal { data -> Signal<(UIImage, UIImage)?, NoError> in
|
||||
|
|
@ -294,6 +295,12 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
|
|||
context.fillPath()
|
||||
}
|
||||
}
|
||||
|
||||
if let cutoutRect {
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: cutoutRect.offsetBy(dx: 0.0, dy: size.height - cutoutRect.maxY - cutoutRect.height))
|
||||
}
|
||||
})
|
||||
let unroundedImage: UIImage?
|
||||
if provideUnrounded {
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ final class BrowserAddressListComponent: Component {
|
|||
|
||||
UIPasteboard.general.string = url
|
||||
if let self, let component = self.component {
|
||||
component.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }))
|
||||
component.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }))
|
||||
}
|
||||
})))
|
||||
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
|
||||
UIPasteboard.general.string = url
|
||||
if let self {
|
||||
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
|
|||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let shareController = ShareController(context: self.context, subject: .url(url))
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
self.present(shareController, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -553,7 +553,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
|
|||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let shareController = ShareController(context: self.context, subject: .url(url))
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
self.present(shareController, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -639,7 +639,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
})
|
||||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
self.controller?.present(shareController, in: .window(.root))
|
||||
case .minimize:
|
||||
|
|
|
|||
|
|
@ -896,8 +896,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
if let data = try? Data(contentsOf: url), let pass = try? PKPass(data: data) {
|
||||
let passLibrary = PKPassLibrary()
|
||||
if passLibrary.containsPass(pass) {
|
||||
//TODO:localize
|
||||
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: "This pass is already added to Wallet.", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})])
|
||||
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: self.presentationData.strings.WebBrowser_PassExistsError, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})])
|
||||
self.present(alertController, nil)
|
||||
} else if let controller = PKAddPassesViewController(pass: pass) {
|
||||
self.getNavigationController()?.view.window?.rootViewController?.present(controller, animated: true)
|
||||
|
|
@ -1339,7 +1338,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}),
|
||||
UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
UIPasteboard.general.string = url.absoluteString
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}),
|
||||
UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
self?.share(url: url.absoluteString)
|
||||
|
|
@ -1393,7 +1392,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let shareController = ShareController(context: self.context, subject: .url(url))
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
self.present(shareController, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ final class CameraDeviceContext {
|
|||
let device = CameraDevice()
|
||||
let input = CameraInput()
|
||||
let output: CameraOutput
|
||||
|
||||
|
||||
init(session: CameraSession, exclusive: Bool, additional: Bool, ciContext: CIContext, colorSpace: CGColorSpace, isRoundVideo: Bool = false) {
|
||||
self.session = session
|
||||
self.exclusive = exclusive
|
||||
|
|
@ -126,7 +126,7 @@ private final class CameraContext {
|
|||
private let audioLevelPipe = ValuePipe<Float>()
|
||||
fileprivate let modeChangePromise = ValuePromise<Camera.ModeChange>(.none)
|
||||
|
||||
var previewView: CameraPreviewView?
|
||||
var videoOutput: CameraVideoOutput?
|
||||
|
||||
var simplePreviewView: CameraSimplePreviewView?
|
||||
var secondaryPreviewView: CameraSimplePreviewView?
|
||||
|
|
@ -310,7 +310,7 @@ private final class CameraContext {
|
|||
|
||||
private var micLevelPeak: Int16 = 0
|
||||
private var micLevelPeakCount = 0
|
||||
|
||||
|
||||
private var isDualCameraEnabled: Bool?
|
||||
public func setDualCameraEnabled(_ enabled: Bool, change: Bool = true) {
|
||||
guard enabled != self.isDualCameraEnabled else {
|
||||
|
|
@ -378,12 +378,20 @@ private final class CameraContext {
|
|||
guard let self, let mainDeviceContext = self.mainDeviceContext else {
|
||||
return
|
||||
}
|
||||
|
||||
var front = false
|
||||
if #available(iOS 13.0, *) {
|
||||
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
|
||||
if sampleBuffer.type == kCMMediaType_Video {
|
||||
Queue.mainQueue().async {
|
||||
self.videoOutput?.push(sampleBuffer, mirror: front)
|
||||
}
|
||||
}
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5, !mainDeviceContext.output.isRecording || !self.savedSnapshot {
|
||||
var front = false
|
||||
if #available(iOS 13.0, *) {
|
||||
front = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, front: front)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
self.savedSnapshot = true
|
||||
|
|
@ -696,6 +704,26 @@ public final class Camera {
|
|||
public typealias ExposureMode = AVCaptureDevice.ExposureMode
|
||||
public typealias FlashMode = AVCaptureDevice.FlashMode
|
||||
|
||||
public struct CollageGrid: Hashable {
|
||||
public struct Row: Hashable {
|
||||
public let columns: Int
|
||||
|
||||
public init(columns: Int) {
|
||||
self.columns = columns
|
||||
}
|
||||
}
|
||||
|
||||
public let rows: [Row]
|
||||
|
||||
public init(rows: [Row]) {
|
||||
self.rows = rows
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return self.rows.reduce(0) { $0 + $1.columns }
|
||||
}
|
||||
}
|
||||
|
||||
public struct Configuration {
|
||||
let preset: Preset
|
||||
let position: Position
|
||||
|
|
@ -975,16 +1003,19 @@ public final class Camera {
|
|||
}
|
||||
}
|
||||
|
||||
public func attachPreviewView(_ view: CameraPreviewView) {
|
||||
self.previewView = view
|
||||
let viewRef: Unmanaged<CameraPreviewView> = Unmanaged.passRetained(view)
|
||||
public func setPreviewOutput(_ output: CameraVideoOutput?) {
|
||||
let outputRef: Unmanaged<CameraVideoOutput>? = output.flatMap { Unmanaged.passRetained($0) }
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.previewView = viewRef.takeUnretainedValue()
|
||||
viewRef.release()
|
||||
if let outputRef {
|
||||
context.videoOutput = outputRef.takeUnretainedValue()
|
||||
outputRef.release()
|
||||
} else {
|
||||
context.videoOutput = nil
|
||||
}
|
||||
} else {
|
||||
Queue.mainQueue().async {
|
||||
viewRef.release()
|
||||
outputRef?.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1108,3 +1139,15 @@ public struct CameraRecordingData {
|
|||
public enum CameraRecordingError {
|
||||
case audioInitializationError
|
||||
}
|
||||
|
||||
public class CameraVideoOutput {
|
||||
private let sink: (CMSampleBuffer, Bool) -> Void
|
||||
|
||||
public init(sink: @escaping (CMSampleBuffer, Bool) -> Void) {
|
||||
self.sink = sink
|
||||
}
|
||||
|
||||
func push(_ buffer: CMSampleBuffer, mirror: Bool) {
|
||||
self.sink(buffer, mirror)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import CoreImage
|
||||
import Vision
|
||||
|
|
@ -286,6 +287,19 @@ final class CameraOutput: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
let image = generateImage(CGSize(width: 1080, height: 1920), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
let colors: [UIColor] = [UIColor(rgb: 0xff00ff), UIColor(rgb: 0xff0000), UIColor(rgb: 0x00ffff), UIColor(rgb: 0x00ff00)]
|
||||
if let randomColor = colors.randomElement() {
|
||||
context.setFillColor(randomColor.cgColor)
|
||||
}
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
return .single(.began)
|
||||
|> then(
|
||||
.single(.finished(image, nil, CACurrentMediaTime())) |> delay(0.5, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
#else
|
||||
let uniqueId = settings.uniqueID
|
||||
let photoCapture = PhotoCaptureContext(ciContext: self.ciContext, settings: settings, orientation: orientation, mirror: mirror)
|
||||
self.photoCaptureRequests[uniqueId] = photoCapture
|
||||
|
|
@ -295,6 +309,7 @@ final class CameraOutput: NSObject {
|
|||
|> afterDisposed { [weak self] in
|
||||
self?.photoCaptureRequests.removeValue(forKey: uniqueId)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var isRecording: Bool {
|
||||
|
|
|
|||
|
|
@ -1182,6 +1182,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
self.openStarsTopup(amount: amount)
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.openWebApp = { [weak self] user in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.openWebApp(
|
||||
context: self.context,
|
||||
parentController: self,
|
||||
updatedPresentationData: self.updatedPresentationData,
|
||||
botPeer: .user(user),
|
||||
chatPeer: nil,
|
||||
threadId: nil,
|
||||
buttonText: "",
|
||||
url: "",
|
||||
simple: true,
|
||||
source: .generic,
|
||||
skipTermsOfService: true,
|
||||
payload: nil
|
||||
)
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -351,6 +351,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
|
|||
itemNode.listNode.openStarsTopup = { [weak self] amount in
|
||||
self?.openStarsTopup?(amount)
|
||||
}
|
||||
itemNode.listNode.openWebApp = { [weak self] amount in
|
||||
self?.openWebApp?(amount)
|
||||
}
|
||||
|
||||
self.currentItemStateValue.set(itemNode.listNode.state |> map { state in
|
||||
let filterId: Int32?
|
||||
|
|
@ -417,6 +420,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
|
|||
var openPremiumManagement: (() -> Void)?
|
||||
var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)?
|
||||
var openStarsTopup: ((Int64?) -> Void)?
|
||||
var openWebApp: ((TelegramUser) -> Void)?
|
||||
var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
|
||||
var didBeginSelectingChats: (() -> Void)?
|
||||
var canExpandHiddenItems: (() -> Bool)?
|
||||
|
|
|
|||
|
|
@ -1493,7 +1493,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||
UIPasteboard.general.string = invite.link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
|
|
|
|||
|
|
@ -1154,7 +1154,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||
UIPasteboard.general.string = linkForCopying
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.present?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
self?.present?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3013,6 +3013,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||
}, openStarsTopup: { _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
}, openWebApp: { _ in
|
||||
})
|
||||
chatListInteraction.isSearchMode = true
|
||||
|
||||
|
|
@ -3936,7 +3937,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||
if !value.isEmpty {
|
||||
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: value, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: navigationController, dismissInput: {})
|
||||
} else {
|
||||
let _ = (context.engine.peers.resolvePeerByName(name: "botfather")
|
||||
let _ = (context.engine.peers.resolvePeerByName(name: "botfather", referrer: nil)
|
||||
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
|
|
@ -4905,6 +4906,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
|
|||
}, openStarsTopup: { _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
}, openWebApp: { _ in
|
||||
})
|
||||
var isInlineMode = false
|
||||
if case .topics = key {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
|
|||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
}, openWebApp: { _ in
|
||||
})
|
||||
interaction.isInlineMode = isInlineMode
|
||||
|
||||
|
|
|
|||
|
|
@ -1234,6 +1234,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
var credibilityIconComponent: EmojiStatusComponent?
|
||||
let mutedIconNode: ASImageNode
|
||||
var itemTagList: ComponentView<Empty>?
|
||||
var actionButtonTitleNode: TextNode?
|
||||
var actionButtonBackgroundView: UIImageView?
|
||||
var actionButtonNode: HighlightableButtonNode?
|
||||
|
||||
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
|
||||
private var cachedDataDisposable = MetaDisposable()
|
||||
|
|
@ -1890,6 +1893,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
let onlineLayout = self.onlineNode.asyncLayout()
|
||||
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
|
||||
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
||||
let makeActionButtonTitleNodeLayout = TextNode.asyncLayout(self.actionButtonTitleNode)
|
||||
|
||||
let currentItem = self.layoutParams?.0
|
||||
let currentChatListText = self.cachedChatListText
|
||||
|
|
@ -2077,6 +2081,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
var currentSecretIconImage: UIImage?
|
||||
var currentForwardedIcon: UIImage?
|
||||
var currentStoryIcon: UIImage?
|
||||
var currentGiftIcon: UIImage?
|
||||
|
||||
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
||||
var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)?
|
||||
|
|
@ -2250,6 +2255,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
var displayForwardedIcon = false
|
||||
var displayStoryReplyIcon = false
|
||||
var displayGiftIcon = false
|
||||
var ignoreForwardedIcon = false
|
||||
|
||||
switch contentData {
|
||||
|
|
@ -2558,6 +2564,22 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
displayForwardedIcon = true
|
||||
} else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) {
|
||||
displayStoryReplyIcon = true
|
||||
} else {
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case .giftPremium, .giftStars, .starGift:
|
||||
displayGiftIcon = true
|
||||
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _):
|
||||
if boostPeerId == nil {
|
||||
displayGiftIcon = true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2712,6 +2734,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
currentStoryIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if displayGiftIcon {
|
||||
currentGiftIcon = PresentationResourcesChatList.giftIcon(item.presentationData.theme)
|
||||
}
|
||||
|
||||
if let currentForwardedIcon {
|
||||
textLeftCutout += currentForwardedIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
|
|
@ -2730,6 +2756,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
if let currentGiftIcon {
|
||||
textLeftCutout += currentGiftIcon.size.width
|
||||
if !contentImageSpecs.isEmpty {
|
||||
textLeftCutout += forwardedIconSpacing
|
||||
} else {
|
||||
textLeftCutout += contentImageTrailingSpace
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0 ..< contentImageSpecs.count {
|
||||
if i != 0 {
|
||||
textLeftCutout += contentImageSpacing
|
||||
|
|
@ -3061,6 +3096,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent)
|
||||
|
||||
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(15.0), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
}
|
||||
|
||||
var badgeSize: CGFloat = 0.0
|
||||
if !badgeLayout.width.isZero {
|
||||
badgeSize += badgeLayout.width + 5.0
|
||||
|
|
@ -3081,6 +3121,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
badgeSize += currentPinnedIconImage.size.width
|
||||
}
|
||||
if let (actionButtonTitleNodeLayout, _) = actionButtonTitleNodeLayoutAndApply {
|
||||
if !badgeSize.isZero {
|
||||
badgeSize += 4.0
|
||||
} else {
|
||||
badgeSize += 5.0
|
||||
}
|
||||
badgeSize += actionButtonTitleNodeLayout.size.width + 12.0 * 2.0
|
||||
}
|
||||
badgeSize = max(badgeSize, reorderInset)
|
||||
|
||||
if !itemTags.isEmpty {
|
||||
|
|
@ -3816,23 +3864,19 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
|
||||
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
|
||||
|
||||
var nextBadgeX: CGFloat = contentRect.maxX
|
||||
if let _ = currentBadgeBackgroundImage {
|
||||
let badgeFrame = CGRect(x: contentRect.maxX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
|
||||
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
|
||||
|
||||
transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame)
|
||||
nextBadgeX -= badgeLayout.width + 6.0
|
||||
}
|
||||
|
||||
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
|
||||
let mentionBadgeOffset: CGFloat
|
||||
if badgeLayout.width.isZero {
|
||||
mentionBadgeOffset = contentRect.maxX - mentionBadgeLayout.width
|
||||
} else {
|
||||
mentionBadgeOffset = contentRect.maxX - badgeLayout.width - 6.0 - mentionBadgeLayout.width
|
||||
}
|
||||
|
||||
let badgeFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
|
||||
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
|
||||
|
||||
transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame)
|
||||
nextBadgeX -= mentionBadgeLayout.width + 6.0
|
||||
}
|
||||
|
||||
if let currentPinnedIconImage = currentPinnedIconImage {
|
||||
|
|
@ -3840,14 +3884,71 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.pinnedIconNode.isHidden = false
|
||||
|
||||
let pinnedIconSize = currentPinnedIconImage.size
|
||||
let pinnedIconFrame = CGRect(x: contentRect.maxX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
|
||||
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
|
||||
|
||||
strongSelf.pinnedIconNode.frame = pinnedIconFrame
|
||||
nextBadgeX -= pinnedIconSize.width + 6.0
|
||||
} else {
|
||||
strongSelf.pinnedIconNode.image = nil
|
||||
strongSelf.pinnedIconNode.isHidden = true
|
||||
}
|
||||
|
||||
if let (actionButtonTitleNodeLayout, apply) = actionButtonTitleNodeLayoutAndApply {
|
||||
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + 12.0 * 2.0, height: actionButtonTitleNodeLayout.size.height + 5.0 + 4.0)
|
||||
let actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.maxY - actionButtonSize.height, width: actionButtonSize.width, height: actionButtonSize.height)
|
||||
|
||||
let actionButtonNode: HighlightableButtonNode
|
||||
if let current = strongSelf.actionButtonNode {
|
||||
actionButtonNode = current
|
||||
} else {
|
||||
actionButtonNode = HighlightableButtonNode()
|
||||
strongSelf.actionButtonNode = actionButtonNode
|
||||
strongSelf.mainContentContainerNode.addSubnode(actionButtonNode)
|
||||
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
let actionButtonBackgroundView: UIImageView
|
||||
if let current = strongSelf.actionButtonBackgroundView {
|
||||
actionButtonBackgroundView = current
|
||||
} else {
|
||||
actionButtonBackgroundView = UIImageView()
|
||||
strongSelf.actionButtonBackgroundView = actionButtonBackgroundView
|
||||
actionButtonNode.view.addSubview(actionButtonBackgroundView)
|
||||
|
||||
if actionButtonBackgroundView.image?.size.height != actionButtonSize.height {
|
||||
actionButtonBackgroundView.image = generateStretchableFilledCircleImage(diameter: actionButtonSize.height, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
actionButtonBackgroundView.tintColor = theme.unreadBadgeActiveBackgroundColor
|
||||
|
||||
let actionButtonTitleNode = apply()
|
||||
if strongSelf.actionButtonTitleNode !== actionButtonTitleNode {
|
||||
strongSelf.actionButtonTitleNode?.removeFromSupernode()
|
||||
strongSelf.actionButtonTitleNode = actionButtonTitleNode
|
||||
actionButtonNode.addSubnode(actionButtonTitleNode)
|
||||
}
|
||||
|
||||
actionButtonNode.frame = actionButtonFrame
|
||||
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
|
||||
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: 5.0), size: actionButtonTitleNodeLayout.size)
|
||||
|
||||
nextBadgeX -= actionButtonSize.width + 6.0
|
||||
} else {
|
||||
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
|
||||
actionButtonTitleNode.removeFromSupernode()
|
||||
strongSelf.actionButtonTitleNode = nil
|
||||
}
|
||||
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
|
||||
actionButtonBackgroundView.removeFromSuperview()
|
||||
strongSelf.actionButtonBackgroundView = nil
|
||||
}
|
||||
if let actionButtonNode = strongSelf.actionButtonNode {
|
||||
actionButtonNode.removeFromSupernode()
|
||||
strongSelf.actionButtonNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
var titleOffset: CGFloat = 0.0
|
||||
if let currentSecretIconImage = currentSecretIconImage {
|
||||
let iconNode: ASImageNode
|
||||
|
|
@ -4190,6 +4291,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
messageTypeIconOffset.y += 3.0
|
||||
} else if let currentStoryIcon {
|
||||
messageTypeIcon = currentStoryIcon
|
||||
} else if let currentGiftIcon {
|
||||
messageTypeIcon = currentGiftIcon
|
||||
messageTypeIconOffset.y -= 2.0 - UIScreenPixel
|
||||
}
|
||||
|
||||
if let messageTypeIcon {
|
||||
|
|
@ -4495,6 +4599,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id)
|
||||
}
|
||||
|
||||
@objc private func actionButtonPressed() {
|
||||
guard let item else {
|
||||
return
|
||||
}
|
||||
guard case let .peer(peerData) = item.content else {
|
||||
return
|
||||
}
|
||||
if case let .user(user) = peerData.peer.peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||
item.interaction.openWebApp(user)
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ public final class ChatListNodeInteraction {
|
|||
let openStarsTopup: (Int64?) -> Void
|
||||
let dismissNotice: (ChatListNotice) -> Void
|
||||
let editPeer: (ChatListItem) -> Void
|
||||
let openWebApp: (TelegramUser) -> Void
|
||||
|
||||
public var searchTextHighightState: String?
|
||||
var highlightedChatLocation: ChatListHighlightedLocation?
|
||||
|
|
@ -167,7 +168,8 @@ public final class ChatListNodeInteraction {
|
|||
openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void,
|
||||
openStarsTopup: @escaping (Int64?) -> Void,
|
||||
dismissNotice: @escaping (ChatListNotice) -> Void,
|
||||
editPeer: @escaping (ChatListItem) -> Void
|
||||
editPeer: @escaping (ChatListItem) -> Void,
|
||||
openWebApp: @escaping (TelegramUser) -> Void
|
||||
) {
|
||||
self.activateSearch = activateSearch
|
||||
self.peerSelected = peerSelected
|
||||
|
|
@ -211,6 +213,7 @@ public final class ChatListNodeInteraction {
|
|||
self.openStarsTopup = openStarsTopup
|
||||
self.dismissNotice = dismissNotice
|
||||
self.editPeer = editPeer
|
||||
self.openWebApp = openWebApp
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -755,7 +758,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||
case .reviewLogin:
|
||||
break
|
||||
case let .starsSubscriptionLowBalance(amount, _):
|
||||
nodeInteraction?.openStarsTopup(amount)
|
||||
nodeInteraction?.openStarsTopup(amount.value)
|
||||
}
|
||||
case .hide:
|
||||
nodeInteraction?.dismissNotice(notice)
|
||||
|
|
@ -1099,7 +1102,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||
case .reviewLogin:
|
||||
break
|
||||
case let .starsSubscriptionLowBalance(amount, _):
|
||||
nodeInteraction?.openStarsTopup(amount)
|
||||
nodeInteraction?.openStarsTopup(amount.value)
|
||||
}
|
||||
case .hide:
|
||||
nodeInteraction?.dismissNotice(notice)
|
||||
|
|
@ -1220,6 +1223,7 @@ public final class ChatListNode: ListView {
|
|||
public var openBirthdaySetup: (() -> Void)?
|
||||
public var openPremiumManagement: (() -> Void)?
|
||||
public var openStarsTopup: ((Int64?) -> Void)?
|
||||
public var openWebApp: ((TelegramUser) -> Void)?
|
||||
|
||||
private var theme: PresentationTheme
|
||||
|
||||
|
|
@ -1867,6 +1871,11 @@ public final class ChatListNode: ListView {
|
|||
break
|
||||
}
|
||||
}, editPeer: { _ in
|
||||
}, openWebApp: { [weak self] user in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openWebApp?(user)
|
||||
})
|
||||
nodeInteraction.isInlineMode = isInlineMode
|
||||
|
||||
|
|
@ -2003,7 +2012,7 @@ public final class ChatListNode: ListView {
|
|||
if let starsSubscriptionsContext {
|
||||
return starsSubscriptionsContext.state
|
||||
|> map { state in
|
||||
if state.balance > 0 && !state.subscriptions.isEmpty {
|
||||
if state.balance > StarsAmount.zero && !state.subscriptions.isEmpty {
|
||||
return .starsSubscriptionLowBalance(
|
||||
amount: state.balance,
|
||||
peers: state.subscriptions.map { $0.peer }
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public enum ChatListNotice: Equatable {
|
|||
case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday])
|
||||
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
|
||||
case premiumGrace
|
||||
case starsSubscriptionLowBalance(amount: Int64, peers: [EnginePeer])
|
||||
case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer])
|
||||
}
|
||||
|
||||
enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
|
|||
case let .starsSubscriptionLowBalance(amount, peers):
|
||||
let title: String
|
||||
let text: String
|
||||
let starsValue = item.strings.ChatList_SubscriptionsLowBalance_Stars(Int32(amount))
|
||||
let starsValue = item.strings.ChatList_SubscriptionsLowBalance_Stars(Int32(amount.value))
|
||||
if let peer = peers.first, peers.count == 1 {
|
||||
title = item.strings.ChatList_SubscriptionsLowBalance_Single_Title(starsValue, peer.compactDisplayTitle).string
|
||||
text = item.strings.ChatList_SubscriptionsLowBalance_Single_Text
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum HStackAlignment {
|
||||
case left
|
||||
case alternatingLeftRight
|
||||
}
|
||||
|
||||
public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
public typealias EnvironmentType = ChildEnvironment
|
||||
|
||||
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
||||
private let spacing: CGFloat
|
||||
private let alignment: HStackAlignment
|
||||
|
||||
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], spacing: CGFloat) {
|
||||
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], spacing: CGFloat, alignment: HStackAlignment = .left) {
|
||||
self.items = items
|
||||
self.spacing = spacing
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
public static func ==(lhs: HStack<ChildEnvironment>, rhs: HStack<ChildEnvironment>) -> Bool {
|
||||
|
|
@ -19,6 +26,9 @@ public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
|
|||
if lhs.spacing != rhs.spacing {
|
||||
return false
|
||||
}
|
||||
if lhs.alignment != rhs.alignment {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -42,21 +52,51 @@ public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
|
|||
}
|
||||
|
||||
var size = CGSize(width: 0.0, height: 0.0)
|
||||
for child in updatedChildren {
|
||||
size.width += child.size.width
|
||||
size.height = max(size.height, child.size.height)
|
||||
}
|
||||
size.width += context.component.spacing * CGFloat(updatedChildren.count - 1)
|
||||
|
||||
var nextX = 0.0
|
||||
for child in updatedChildren {
|
||||
context.add(child
|
||||
.position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center)
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
nextX += child.size.width
|
||||
nextX += context.component.spacing
|
||||
switch context.component.alignment {
|
||||
case .left:
|
||||
for child in updatedChildren {
|
||||
size.width += child.size.width
|
||||
size.height = max(size.height, child.size.height)
|
||||
}
|
||||
size.width += context.component.spacing * CGFloat(updatedChildren.count - 1)
|
||||
|
||||
var nextX = 0.0
|
||||
for child in updatedChildren {
|
||||
context.add(child
|
||||
.position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center)
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
nextX += child.size.width
|
||||
nextX += context.component.spacing
|
||||
}
|
||||
case .alternatingLeftRight:
|
||||
size.width = context.availableSize.width
|
||||
for child in updatedChildren {
|
||||
size.height = max(size.height, child.size.height)
|
||||
}
|
||||
|
||||
var nextLeftX = 0.0
|
||||
var nextRightX = size.width
|
||||
for i in 0 ..< updatedChildren.count {
|
||||
let child = updatedChildren[i]
|
||||
let childFrame: CGRect
|
||||
if i % 2 == 0 {
|
||||
childFrame = CGRect(origin: CGPoint(x: nextLeftX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)
|
||||
nextLeftX += child.size.width
|
||||
nextLeftX += context.component.spacing
|
||||
} else {
|
||||
childFrame = CGRect(origin: CGPoint(x: nextRightX - child.size.width, y: floor((size.height - child.size.height) * 0.5)), size: child.size)
|
||||
nextRightX -= child.size.width
|
||||
nextRightX -= context.component.spacing
|
||||
}
|
||||
|
||||
context.add(child
|
||||
.position(child.size.centered(in: childFrame).center)
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class TransformContents<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
public typealias EnvironmentType = ChildEnvironment
|
||||
|
||||
private let content: AnyComponent<ChildEnvironment>
|
||||
private let fixedSize: CGSize?
|
||||
private let translation: CGPoint
|
||||
|
||||
public init(content: AnyComponent<ChildEnvironment>, fixedSize: CGSize? = nil, translation: CGPoint) {
|
||||
self.content = content
|
||||
self.fixedSize = fixedSize
|
||||
self.translation = translation
|
||||
}
|
||||
|
||||
public static func ==(lhs: TransformContents<ChildEnvironment>, rhs: TransformContents<ChildEnvironment>) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.fixedSize != rhs.fixedSize {
|
||||
return false
|
||||
}
|
||||
if lhs.translation != rhs.translation {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static var body: Body {
|
||||
let child = Child(environment: ChildEnvironment.self)
|
||||
|
||||
return { context in
|
||||
let child = child.update(
|
||||
component: context.component.content,
|
||||
environment: { context.environment[ChildEnvironment.self] },
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let size = context.component.fixedSize ?? child.size
|
||||
|
||||
var childFrame = child.size.centered(in: CGRect(origin: CGPoint(), size: size))
|
||||
childFrame.origin.x += context.component.translation.x
|
||||
childFrame.origin.y += context.component.translation.y
|
||||
|
||||
context.add(child
|
||||
.position(childFrame.center)
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,13 +8,15 @@ public final class BundleIconComponent: Component {
|
|||
public let name: String
|
||||
public let tintColor: UIColor?
|
||||
public let maxSize: CGSize?
|
||||
public let scaleFactor: CGFloat
|
||||
public let shadowColor: UIColor?
|
||||
public let shadowBlur: CGFloat
|
||||
|
||||
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) {
|
||||
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) {
|
||||
self.name = name
|
||||
self.tintColor = tintColor
|
||||
self.maxSize = maxSize
|
||||
self.scaleFactor = scaleFactor
|
||||
self.shadowColor = shadowColor
|
||||
self.shadowBlur = shadowBlur
|
||||
}
|
||||
|
|
@ -29,6 +31,9 @@ public final class BundleIconComponent: Component {
|
|||
if lhs.maxSize != rhs.maxSize {
|
||||
return false
|
||||
}
|
||||
if lhs.scaleFactor != rhs.scaleFactor {
|
||||
return false
|
||||
}
|
||||
if lhs.shadowColor != rhs.shadowColor {
|
||||
return false
|
||||
}
|
||||
|
|
@ -75,6 +80,10 @@ public final class BundleIconComponent: Component {
|
|||
if let maxSize = component.maxSize {
|
||||
imageSize = imageSize.aspectFitted(maxSize)
|
||||
}
|
||||
if component.scaleFactor != 1.0 {
|
||||
imageSize.width = floor(imageSize.width * component.scaleFactor)
|
||||
imageSize.height = floor(imageSize.height * component.scaleFactor)
|
||||
}
|
||||
|
||||
return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -438,6 +438,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
|||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case let .starsReactions(topCount):
|
||||
//TODO:localize
|
||||
self.action = nil
|
||||
self.text = "Send \(topCount) or more to highlight your profile"
|
||||
self.targetSelectionIndex = nil
|
||||
|
|
@ -445,10 +446,15 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
|||
isUserInteractionEnabled = action != nil
|
||||
case .videoProcessing:
|
||||
self.action = nil
|
||||
self.text = "The video will be published once converted and optimized."
|
||||
self.text = self.presentationData.strings.Chat_VideoProcessingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case .collageReordering:
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Camera_CollageReorderingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
|
|
|
|||
|
|
@ -2359,6 +2359,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||
case notificationTopicExceptions(text: String, action: (() -> Void)?)
|
||||
case starsReactions(topCount: Int)
|
||||
case videoProcessing
|
||||
case collageReordering
|
||||
|
||||
public static func ==(lhs: Tip, rhs: Tip) -> Bool {
|
||||
switch lhs {
|
||||
|
|
@ -2416,6 +2417,12 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case .collageReordering:
|
||||
if case .collageReordering = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ swift_library(
|
|||
"//submodules/GZip:GZip",
|
||||
"//third-party/ZipArchive:ZipArchive",
|
||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||
"//submodules/TelegramVoip",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import AppBundle
|
|||
import ZipArchive
|
||||
import WebKit
|
||||
import InAppPurchaseManager
|
||||
import TelegramVoip
|
||||
|
||||
@objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate {
|
||||
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
|
|
@ -104,8 +105,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
case disableReloginTokens(Bool)
|
||||
case disableCallV2(Bool)
|
||||
case experimentalCallMute(Bool)
|
||||
case liveStreamV2(Bool)
|
||||
case dynamicStreaming(Bool)
|
||||
case autoBenchmarkReflectors(Bool)
|
||||
case benchmarkReflectors
|
||||
case enableLocalTranslation(Bool)
|
||||
case preferredVideoCodec(Int, String, String?, Bool)
|
||||
case disableVideoAspectScaling(Bool)
|
||||
|
|
@ -131,7 +132,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, .liveStreamV2, .dynamicStreaming, .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, .autoBenchmarkReflectors, .benchmarkReflectors, .enableLocalTranslation:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
|
|
@ -248,9 +249,9 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return 51
|
||||
case .experimentalCallMute:
|
||||
return 52
|
||||
case .liveStreamV2:
|
||||
case .autoBenchmarkReflectors:
|
||||
return 53
|
||||
case .dynamicStreaming:
|
||||
case .benchmarkReflectors:
|
||||
return 54
|
||||
case .enableLocalTranslation:
|
||||
return 55
|
||||
|
|
@ -1344,25 +1345,70 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
})
|
||||
}).start()
|
||||
})
|
||||
case let .liveStreamV2(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Live Stream V2", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
case let .autoBenchmarkReflectors(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Auto-Benchmark Reflectors [Restart]", 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.liveStreamV2 = value
|
||||
settings.autoBenchmarkReflectors = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .dynamicStreaming(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Dynamic Streaming", 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.dynamicStreaming = value
|
||||
return PreferencesEntry(settings)
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
var signal: Signal<ReflectorBenchmark.Results, NoError> = Signal { subscriber in
|
||||
var reflectorBenchmark: ReflectorBenchmark? = ReflectorBenchmark(address: "91.108.13.35", port: 599)
|
||||
reflectorBenchmark?.start(completion: { results in
|
||||
subscriber.putNext(results)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}).start()
|
||||
|
||||
return ActionDisposable {
|
||||
reflectorBenchmark = nil
|
||||
}
|
||||
}
|
||||
|> runOn(.mainQueue())
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
arguments.presentController(controller, nil)
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
let reindexDisposable = MetaDisposable()
|
||||
|
||||
signal = signal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
reindexDisposable.set(nil)
|
||||
}
|
||||
reindexDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { results in
|
||||
if let context = arguments.context {
|
||||
let controller = textAlertController(context: context, title: nil, text: "Bandwidth: \(results.bandwidthBytesPerSecond * 8 / 1024) kbit/s (expected \(results.expectedBandwidthBytesPerSecond * 8 / 1024) kbit/s)\nAvg latency: \(Int(results.averageDelay * 1000.0)) ms", actions: [TextAlertAction(type: .genericAction, title: "OK", action: {})])
|
||||
arguments.presentController(controller, nil)
|
||||
}
|
||||
}))
|
||||
})
|
||||
case let .enableLocalTranslation(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Local Translation", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
|
|
@ -1530,8 +1576,14 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
|||
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
|
||||
entries.append(.disableCallV2(experimentalSettings.disableCallV2))
|
||||
entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute))
|
||||
entries.append(.liveStreamV2(experimentalSettings.liveStreamV2))
|
||||
entries.append(.dynamicStreaming(experimentalSettings.dynamicStreaming))
|
||||
|
||||
var defaultAutoBenchmarkReflectors = false
|
||||
if case .internal = sharedContext.applicationBindings.appBuildType {
|
||||
defaultAutoBenchmarkReflectors = true
|
||||
}
|
||||
entries.append(.autoBenchmarkReflectors(experimentalSettings.autoBenchmarkReflectors ?? defaultAutoBenchmarkReflectors))
|
||||
|
||||
entries.append(.benchmarkReflectors)
|
||||
entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2529,7 +2529,16 @@ open class TextNode: ASDisplayNode, TextNodeProtocol {
|
|||
textColor = color
|
||||
}
|
||||
}
|
||||
if let textColor {
|
||||
if image.renderingMode == .alwaysOriginal {
|
||||
let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - image.size.width * 0.5, y: attachment.frame.midY - image.size.height * 0.5 + 1.0), size: image.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
|
||||
context.translateBy(x: imageRect.midX, y: imageRect.midY)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
|
||||
context.draw(image.cgImage!, in: imageRect)
|
||||
context.translateBy(x: imageRect.midX, y: imageRect.midY)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
|
||||
} else if let textColor {
|
||||
if let tintedImage = generateTintedImage(image: image, color: textColor) {
|
||||
let imageRect = CGRect(origin: CGPoint(x: attachment.frame.midX - tintedImage.size.width * 0.5, y: attachment.frame.midY - tintedImage.size.height * 0.5 + 1.0), size: tintedImage.size).offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
|
||||
context.translateBy(x: imageRect.midX, y: imageRect.midY)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ objc_library(
|
|||
deps = [
|
||||
"//submodules/ffmpeg",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"CoreMedia",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ extern int FFMpegCodecIdAV1;
|
|||
- (instancetype)init;
|
||||
|
||||
- (void)setIOContext:(FFMpegAVIOContext *)ioContext;
|
||||
- (bool)openInput;
|
||||
- (bool)openInputWithDirectFilePath:(NSString * _Nullable)directFilePath;
|
||||
- (bool)findStreamInfo;
|
||||
- (void)seekFrameForStreamIndex:(int32_t)streamIndex pts:(int64_t)pts positionOnKeyframe:(bool)positionOnKeyframe;
|
||||
- (bool)readFrameIntoPacket:(FFMpegPacket *)packet;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ extern int FFMPEG_CONSTANT_AVERROR_EOF;
|
|||
|
||||
@interface FFMpegAVIOContext : NSObject
|
||||
|
||||
- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable;
|
||||
- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t const * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable;
|
||||
|
||||
- (void *)impl;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#import <FFMpegBinding/FFMpegAVFrame.h>
|
||||
#import <FFMpegBinding/FFMpegAVCodec.h>
|
||||
|
||||
#import "libavformat/avformat.h"
|
||||
#import "libavcodec/avcodec.h"
|
||||
|
||||
@interface FFMpegAVCodecContext () {
|
||||
|
|
@ -35,11 +36,11 @@
|
|||
}
|
||||
|
||||
- (int32_t)channels {
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
return (int32_t)_impl->ch_layout.nb_channels;
|
||||
#else
|
||||
#else
|
||||
return (int32_t)_impl->channels;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
- (int32_t)sampleRate {
|
||||
|
|
|
|||
|
|
@ -39,10 +39,15 @@ int FFMpegCodecIdAV1 = AV_CODEC_ID_AV1;
|
|||
_impl->pb = [ioContext impl];
|
||||
}
|
||||
|
||||
- (bool)openInput {
|
||||
- (bool)openInputWithDirectFilePath:(NSString * _Nullable)directFilePath {
|
||||
AVDictionary *options = nil;
|
||||
av_dict_set(&options, "usetoc", "1", 0);
|
||||
int result = avformat_open_input(&_impl, "file", nil, &options);
|
||||
|
||||
const char *url = "file";
|
||||
if (directFilePath) {
|
||||
url = [directFilePath UTF8String];
|
||||
}
|
||||
int result = avformat_open_input(&_impl, url, nil, &options);
|
||||
av_dict_free(&options);
|
||||
if (_impl != nil) {
|
||||
_impl->flags |= AVFMT_FLAG_FAST_SEEK;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ int FFMPEG_CONSTANT_AVERROR_EOF = AVERROR_EOF;
|
|||
|
||||
@implementation FFMpegAVIOContext
|
||||
|
||||
- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable {
|
||||
- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t const * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
void *avIoBuffer = av_malloc(bufferSize);
|
||||
|
|
@ -21,7 +21,7 @@ int FFMPEG_CONSTANT_AVERROR_EOF = AVERROR_EOF;
|
|||
av_free(avIoBuffer);
|
||||
return nil;
|
||||
}
|
||||
_impl->direct = 1;
|
||||
_impl->direct = 0;
|
||||
|
||||
if (!isSeekable) {
|
||||
_impl->seekable = 0;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#import <FFMpegBinding/FFMpegAVFrame.h>
|
||||
|
||||
#import "libavformat/avformat.h"
|
||||
#import "libavcodec/avcodec.h"
|
||||
#import "libswresample/swresample.h"
|
||||
|
||||
|
|
@ -53,8 +54,29 @@
|
|||
_context = NULL;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
AVChannelLayout channelLayoutIn;
|
||||
av_channel_layout_default(&channelLayoutIn, channelCount);
|
||||
|
||||
AVChannelLayout channelLayoutOut;
|
||||
av_channel_layout_default(&channelLayoutOut, _destinationChannelCount);
|
||||
|
||||
if (swr_alloc_set_opts2(
|
||||
&_context,
|
||||
&channelLayoutOut,
|
||||
(enum AVSampleFormat)_destinationSampleFormat,
|
||||
(int)_destinationSampleRate,
|
||||
&channelLayoutIn,
|
||||
(enum AVSampleFormat)_sourceSampleFormat,
|
||||
(int)_sourceSampleRate,
|
||||
0,
|
||||
NULL
|
||||
) != 0) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_context = swr_alloc_set_opts(NULL,
|
||||
av_get_default_channel_layout((int)_destinationChannelCount),
|
||||
(enum AVSampleFormat)_destinationSampleFormat,
|
||||
|
|
@ -64,7 +86,8 @@
|
|||
(int)_sourceSampleRate,
|
||||
0,
|
||||
NULL);
|
||||
#pragma clang diagnostic pop
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
_currentSourceChannelCount = channelCount;
|
||||
_ratio = MAX(1, _destinationSampleRate / MAX(_sourceSampleRate, 1)) * MAX(1, _destinationChannelCount / channelCount) * 2;
|
||||
if (_context) {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,10 @@
|
|||
|
||||
_stream = avformat_new_stream(_formatContext, codec);
|
||||
if (!_stream) {
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
#else
|
||||
avcodec_close(_codecContext);
|
||||
#endif
|
||||
avcodec_free_context(&_codecContext);
|
||||
_codecContext = nil;
|
||||
avio_closep(&_formatContext->pb);
|
||||
|
|
@ -97,7 +100,10 @@
|
|||
|
||||
int ret = avcodec_parameters_from_context(_stream->codecpar, _codecContext);
|
||||
if (ret < 0) {
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
#else
|
||||
avcodec_close(_codecContext);
|
||||
#endif
|
||||
avcodec_free_context(&_codecContext);
|
||||
_codecContext = nil;
|
||||
avio_closep(&_formatContext->pb);
|
||||
|
|
@ -108,7 +114,10 @@
|
|||
|
||||
ret = avformat_write_header(_formatContext, nil);
|
||||
if (ret < 0) {
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
#else
|
||||
avcodec_close(_codecContext);
|
||||
#endif
|
||||
avcodec_free_context(&_codecContext);
|
||||
_codecContext = nil;
|
||||
avio_closep(&_formatContext->pb);
|
||||
|
|
@ -139,29 +148,30 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
AVPacket pkt;
|
||||
av_init_packet(&pkt);
|
||||
pkt.data = nil;
|
||||
pkt.size = 0;
|
||||
AVPacket *pkt = nil;
|
||||
pkt = av_packet_alloc();
|
||||
pkt->data = nil;
|
||||
pkt->size = 0;
|
||||
|
||||
while (sendRet >= 0) {
|
||||
int recvRet = avcodec_receive_packet(_codecContext, &pkt);
|
||||
int recvRet = avcodec_receive_packet(_codecContext, pkt);
|
||||
if (recvRet == AVERROR(EAGAIN) || recvRet == AVERROR_EOF) {
|
||||
break;
|
||||
} else if (recvRet < 0) {
|
||||
av_packet_unref(&pkt);
|
||||
av_packet_unref(pkt);
|
||||
break;
|
||||
}
|
||||
|
||||
av_packet_rescale_ts(&pkt, _codecContext->time_base, _stream->time_base);
|
||||
pkt.stream_index = _stream->index;
|
||||
av_packet_rescale_ts(pkt, _codecContext->time_base, _stream->time_base);
|
||||
pkt->stream_index = _stream->index;
|
||||
|
||||
int ret = av_interleaved_write_frame(_formatContext, &pkt);
|
||||
av_packet_unref(&pkt);
|
||||
int ret = av_interleaved_write_frame(_formatContext, pkt);
|
||||
av_packet_unref(pkt);
|
||||
if (ret < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
av_packet_free(&pkt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -173,18 +183,20 @@
|
|||
|
||||
int sendRet = avcodec_send_frame(_codecContext, NULL);
|
||||
if (sendRet >= 0) {
|
||||
AVPacket pkt;
|
||||
av_init_packet(&pkt);
|
||||
pkt.data = nil;
|
||||
pkt.size = 0;
|
||||
AVPacket *pkt = nil;
|
||||
pkt = av_packet_alloc();
|
||||
pkt->data = nil;
|
||||
pkt->size = 0;
|
||||
|
||||
while (avcodec_receive_packet(_codecContext, &pkt) == 0) {
|
||||
av_packet_rescale_ts(&pkt, _codecContext->time_base, _stream->time_base);
|
||||
pkt.stream_index = _stream->index;
|
||||
while (avcodec_receive_packet(_codecContext, pkt) == 0) {
|
||||
av_packet_rescale_ts(pkt, _codecContext->time_base, _stream->time_base);
|
||||
pkt->stream_index = _stream->index;
|
||||
|
||||
av_interleaved_write_frame(_formatContext, &pkt);
|
||||
av_packet_unref(&pkt);
|
||||
av_interleaved_write_frame(_formatContext, pkt);
|
||||
av_packet_unref(pkt);
|
||||
}
|
||||
|
||||
av_packet_free(&pkt);
|
||||
}
|
||||
|
||||
if (_formatContext) {
|
||||
|
|
@ -196,7 +208,10 @@
|
|||
}
|
||||
|
||||
if (_codecContext) {
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 59
|
||||
#else
|
||||
avcodec_close(_codecContext);
|
||||
#endif
|
||||
avcodec_free_context(&_codecContext);
|
||||
_codecContext = nil;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1244,7 +1244,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode {
|
|||
|
||||
let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if query.isSingleEmoji {
|
||||
signals = .single([context.engine.stickers.searchStickers(query: [text.basicEmoji.0])
|
||||
signals = .single([context.engine.stickers.searchStickers(query: nil, emoticon: [text.basicEmoji.0], inputLanguageCode: "")
|
||||
|> map { (nil, $0.items) }])
|
||||
} else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" {
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3)
|
||||
|
|
@ -1260,17 +1260,11 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
signals = signal
|
||||
|> map { keywords -> [Signal<(String?, [FoundStickerItem]), NoError>] in
|
||||
var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = []
|
||||
let emoticons = keywords.flatMap { $0.emoticons }
|
||||
for emoji in emoticons {
|
||||
signals.append(context.engine.stickers.searchStickers(query: [emoji.basicEmoji.0])
|
||||
|> take(1)
|
||||
|> map { (emoji, $0.items) })
|
||||
}
|
||||
return signals
|
||||
let emoticon = keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 }
|
||||
return [context.engine.stickers.searchStickers(query: query, emoticon: emoticon, inputLanguageCode: languageCode)
|
||||
|> map { (nil, $0.items) }]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public enum ChatMessageGalleryControllerData {
|
|||
case pass(TelegramMediaFile)
|
||||
case instantPage(InstantPageGalleryController, Int, Media)
|
||||
case map(TelegramMediaMap)
|
||||
case stickerPack(StickerPackReference)
|
||||
case stickerPack(StickerPackReference, TelegramMediaFile?)
|
||||
case audio(TelegramMediaFile)
|
||||
case document(TelegramMediaFile, Bool)
|
||||
case gallery(Signal<GalleryController, NoError>)
|
||||
|
|
@ -104,7 +104,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
for attribute in file.attributes {
|
||||
if case let .CustomEmoji(_, _, _, reference) = attribute {
|
||||
if let reference = reference {
|
||||
return .stickerPack(reference)
|
||||
return .stickerPack(reference, file)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -214,7 +214,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
for attribute in file.attributes {
|
||||
if case let .Sticker(_, reference, _) = attribute {
|
||||
if let reference = reference {
|
||||
return .stickerPack(reference)
|
||||
return .stickerPack(reference, file)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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))
|
||||
} 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))
|
||||
}
|
||||
|
|
@ -1050,7 +1050,7 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
} else if isEmail {
|
||||
content = .copy(text: presentationData.strings.Conversation_EmailCopied)
|
||||
} else if canAddToReadingList {
|
||||
content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied)
|
||||
content = .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied)
|
||||
} else {
|
||||
content = .copy(text: presentationData.strings.Conversation_TextCopied)
|
||||
}
|
||||
|
|
@ -1209,9 +1209,9 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
|||
Queue.mainQueue().after(0.2, {
|
||||
let content: UndoOverlayContent
|
||||
if warnAboutPrivate {
|
||||
content = .linkCopied(text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong)
|
||||
content = .linkCopied(title: nil, text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong)
|
||||
} else {
|
||||
content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied)
|
||||
content = .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied)
|
||||
}
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1839,7 +1839,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
if let status = status {
|
||||
let shouldStorePlaybacksState: Bool
|
||||
#if DEBUG
|
||||
#if DEBUG && false
|
||||
shouldStorePlaybacksState = status.duration >= 10.0
|
||||
#else
|
||||
shouldStorePlaybacksState = status.duration >= 60.0 * 10.0
|
||||
|
|
@ -3569,14 +3569,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { [weak self] _, f in
|
||||
}, action: { [weak strongSelf] _, f in
|
||||
f(.default)
|
||||
|
||||
guard let self, let videoNode = self.videoNode else {
|
||||
guard let strongSelf, let videoNode = strongSelf.videoNode else {
|
||||
return
|
||||
}
|
||||
videoNode.setVideoQuality(.auto)
|
||||
self.videoQualityPromise.set(.auto)
|
||||
strongSelf.videoQualityPromise.set(.auto)
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
@ -3595,7 +3595,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
} else {
|
||||
qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityQHD
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: qualityTitle, textLayout: .secondLineWithValue("\(quality)p"), icon: { _ in
|
||||
var qualityDebugText = ""
|
||||
var displayDebugInfo = false
|
||||
if strongSelf.context.sharedContext.applicationBindings.appBuildType == .internal {
|
||||
displayDebugInfo = true
|
||||
} else {
|
||||
#if DEBUG
|
||||
displayDebugInfo = true
|
||||
#endif
|
||||
}
|
||||
if displayDebugInfo, let content = item.content as? HLSVideoContent, let qualitySet = HLSQualitySet(baseFile: content.fileReference, codecConfiguration: HLSCodecConfiguration(context: strongSelf.context)), let qualityFile = qualitySet.qualityFiles[quality] {
|
||||
for attribute in qualityFile.media.attributes {
|
||||
if case let .Video(_, _, _, _, _, videoCodec) = attribute, let videoCodec {
|
||||
qualityDebugText += " \(videoCodec)"
|
||||
if videoCodec == "av1" || videoCodec == "av01" {
|
||||
qualityDebugText += isHardwareAv1Supported ? " (HW)" : " (SW)"
|
||||
}
|
||||
}
|
||||
}
|
||||
if let size = qualityFile.media.size {
|
||||
qualityDebugText += ", \(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: strongSelf.presentationData)))"
|
||||
}
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: qualityTitle, textLayout: .secondLineWithValue("\(quality)p\(qualityDebugText)"), icon: { _ in
|
||||
if isSelected {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
|
||||
} else {
|
||||
|
|
@ -3632,7 +3654,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
if qualityState.available.isEmpty {
|
||||
return
|
||||
}
|
||||
guard let qualitySet = HLSQualitySet(baseFile: content.fileReference) else {
|
||||
guard let qualitySet = HLSQualitySet(baseFile: content.fileReference, codecConfiguration: HLSCodecConfiguration(context: self.context)) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ final class GameControllerNode: ViewControllerTracingNode {
|
|||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
}
|
||||
self.present(shareController, nil)
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
}
|
||||
shareController.completed = { [weak self] peerIds in
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ final class InstantPageFeedbackNode: ASDisplayNode, InstantPageNode {
|
|||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
self.resolveDisposable.set((self.context.engine.peers.resolvePeerByName(name: "previews")
|
||||
self.resolveDisposable.set((self.context.engine.peers.resolvePeerByName(name: "previews", referrer: nil)
|
||||
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ public final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode
|
|||
let signal: Signal<EnginePeer, NoError> = actualizedPeer(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, peer: initialPeer._asPeer())
|
||||
|> mapToSignal({ peer -> Signal<EnginePeer, NoError> in
|
||||
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
|
||||
return .single(.channel(peer)) |> then(engine.peers.resolvePeerByName(name: username)
|
||||
return .single(.channel(peer)) |> then(engine.peers.resolvePeerByName(name: username, referrer: nil)
|
||||
|> mapToSignal({ result -> Signal<EnginePeer, NoError> in
|
||||
guard case let .result(updatedPeer) = result else {
|
||||
return .complete()
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||
}
|
||||
shareController.actionCompleted = {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}, openMainLink: { _ in
|
||||
|
|
@ -415,7 +415,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||
dismissTooltipsImpl?()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}, mainLinkContextAction: { invite, node, gesture in
|
||||
guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?(), let invite = invite else {
|
||||
return
|
||||
|
|
@ -454,7 +454,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||
UIPasteboard.general.string = invite.link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
|||
|
||||
|
||||
case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool)
|
||||
case subscriptionFee(PresentationTheme, String, Bool, Int64?, String, Int64?)
|
||||
case subscriptionFee(PresentationTheme, String, Bool, StarsAmount?, String, StarsAmount?)
|
||||
case subscriptionFeeInfo(PresentationTheme, String)
|
||||
|
||||
case requestApproval(PresentationTheme, String, Bool, Bool)
|
||||
|
|
@ -328,7 +328,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
|
|||
return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, label: label, type: .number, spacing: 3.0, enabled: enabled, tag: InviteLinksEditEntryTag.subscriptionFee, sectionId: self.section, textUpdated: { text in
|
||||
arguments.updateState { state in
|
||||
var updatedState = state
|
||||
if var value = Int64(text) {
|
||||
if var value = Int64(text).flatMap({ StarsAmount(value: $0, nanos: 0) }) {
|
||||
if let maxValue, value > maxValue {
|
||||
value = maxValue
|
||||
arguments.errorWithItem(.subscriptionFee)
|
||||
|
|
@ -492,14 +492,14 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
|
|||
entries.append(.subscriptionFeeToggle(presentationData.theme, presentationData.strings.InviteLink_Create_Fee, state.subscriptionEnabled, isEditingEnabled))
|
||||
if state.subscriptionEnabled {
|
||||
var label: String = ""
|
||||
if let subscriptionFee = state.subscriptionFee, subscriptionFee > 0 {
|
||||
if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero {
|
||||
var usdRate = 0.012
|
||||
if let usdWithdrawRate = configuration.usdWithdrawRate {
|
||||
usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
|
||||
}
|
||||
label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
|
||||
label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
|
||||
}
|
||||
entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, configuration.maxFee))
|
||||
entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, configuration.maxFee.flatMap({ StarsAmount(value: $0, nanos: 0) })))
|
||||
}
|
||||
let infoText: String
|
||||
if let _ = invite, state.subscriptionEnabled {
|
||||
|
|
@ -566,7 +566,7 @@ private struct InviteLinkEditControllerState: Equatable {
|
|||
var time: InviteLinkTimeLimit
|
||||
var requestApproval = false
|
||||
var subscriptionEnabled = false
|
||||
var subscriptionFee: Int64?
|
||||
var subscriptionFee: StarsAmount?
|
||||
var pickingExpiryDate = false
|
||||
var pickingExpiryTime = false
|
||||
var pickingUsageLimit = false
|
||||
|
|
@ -698,7 +698,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio
|
|||
|
||||
var doneIsEnabled = true
|
||||
if state.subscriptionEnabled {
|
||||
if (state.subscriptionFee ?? 0) == 0 {
|
||||
if (state.subscriptionFee ?? StarsAmount.zero) == StarsAmount.zero {
|
||||
doneIsEnabled = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ public final class InviteLinkInviteController: ViewController {
|
|||
self?.controller?.dismissAllTooltips()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
})))
|
||||
|
||||
|
|
@ -426,7 +426,7 @@ public final class InviteLinkInviteController: ViewController {
|
|||
self?.controller?.dismissAllTooltips()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}, shareLink: { [weak self] invite in
|
||||
guard let strongSelf = self, let inviteLink = invite.link else {
|
||||
return
|
||||
|
|
@ -488,7 +488,7 @@ public final class InviteLinkInviteController: ViewController {
|
|||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
}
|
||||
strongSelf.controller?.present(shareController, in: .window(.root))
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
|||
}
|
||||
shareController.actionCompleted = {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}, openMainLink: { invite in
|
||||
|
|
@ -495,7 +495,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
|||
dismissTooltipsImpl?()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}, mainLinkContextAction: { invite, node, gesture in
|
||||
guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?(), let invite = invite else {
|
||||
return
|
||||
|
|
@ -513,7 +513,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
|||
UIPasteboard.general.string = invite.link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
||||
|
|
@ -634,7 +634,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
|||
UIPasteboard.general.string = invite.link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
})))
|
||||
|
||||
if !invite.isRevoked {
|
||||
|
|
@ -699,7 +699,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio
|
|||
}
|
||||
shareController.actionCompleted = {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
presentControllerImpl?(shareController, nil)
|
||||
})))
|
||||
|
|
|
|||
|
|
@ -607,7 +607,7 @@ public final class InviteLinkViewController: ViewController {
|
|||
self?.controller?.dismissAllTooltips()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}, shareLink: { [weak self] invite in
|
||||
guard let inviteLink = invite.link else {
|
||||
return
|
||||
|
|
@ -667,7 +667,7 @@ public final class InviteLinkViewController: ViewController {
|
|||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
self?.controller?.present(shareController, in: .window(.root))
|
||||
}, editLink: { [weak self] invite in
|
||||
|
|
@ -707,7 +707,7 @@ public final class InviteLinkViewController: ViewController {
|
|||
self?.controller?.dismissAllTooltips()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
})))
|
||||
|
||||
if invite.isRevoked {
|
||||
|
|
@ -849,7 +849,7 @@ public final class InviteLinkViewController: ViewController {
|
|||
var subtitle = presentationData.strings.InviteLink_SubscriptionFee_NoOneJoined
|
||||
if state.count > 0 {
|
||||
title += " x \(state.count)"
|
||||
let usdValue = formatTonUsdValue(pricing.amount * Int64(state.count), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
let usdValue = formatTonUsdValue(pricing.amount.value * Int64(state.count), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
subtitle = presentationData.strings.InviteLink_SubscriptionFee_ApproximateIncome(usdValue).string
|
||||
}
|
||||
entries.append(.subscriptionPricing(presentationData.theme, title, subtitle))
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import AvatarNode
|
|||
import TelegramCore
|
||||
import AccountContext
|
||||
import TextNodeWithEntities
|
||||
import ListItemComponentAdaptor
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 16.0)
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ public enum ItemListDisclosureItemDetailLabelColor {
|
|||
case destructive
|
||||
}
|
||||
|
||||
public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
||||
public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
|
||||
let presentationData: ItemListPresentationData
|
||||
let icon: UIImage?
|
||||
let context: AccountContext?
|
||||
|
|
@ -56,6 +57,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
|||
let titleColor: ItemListDisclosureItemTitleColor
|
||||
let titleFont: ItemListDisclosureItemTitleFont
|
||||
let titleIcon: UIImage?
|
||||
let titleBadge: String?
|
||||
let enabled: Bool
|
||||
let label: String
|
||||
let attributedLabel: NSAttributedString?
|
||||
|
|
@ -71,7 +73,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
|||
public let tag: ItemListItemTag?
|
||||
public let shimmeringIndex: Int?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.icon = icon
|
||||
self.context = context
|
||||
|
|
@ -81,6 +83,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
|||
self.titleColor = titleColor
|
||||
self.titleFont = titleFont
|
||||
self.titleIcon = titleIcon
|
||||
self.titleBadge = titleBadge
|
||||
self.enabled = enabled
|
||||
self.labelStyle = labelStyle
|
||||
self.label = label
|
||||
|
|
@ -140,6 +143,27 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
|
|||
self.action?()
|
||||
}
|
||||
}
|
||||
|
||||
public func item() -> ListViewItem {
|
||||
return self
|
||||
}
|
||||
|
||||
public static func ==(lhs: ItemListDisclosureItem, rhs: ItemListDisclosureItem) -> Bool {
|
||||
if lhs.presentationData != rhs.presentationData {
|
||||
return false
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.label != rhs.label {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private let badgeFont = Font.regular(15.0)
|
||||
|
|
@ -162,6 +186,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
|||
let labelBadgeNode: ASImageNode
|
||||
let labelImageNode: ASImageNode
|
||||
|
||||
var titleBadgeNode: ASImageNode?
|
||||
var titleBadgeTextNode: TextNode?
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: ItemListDisclosureItem?
|
||||
|
|
@ -260,6 +287,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
|||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode)
|
||||
|
||||
let makeTitleBadgeTextNodeLayout = TextNode.asyncLayout(self.titleBadgeTextNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
let currentHasBadge = self.labelBadgeNode.image != nil
|
||||
|
|
@ -374,6 +403,13 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
|||
maxTitleWidth -= 12.0
|
||||
}
|
||||
|
||||
var titleBadgeTextNodeLayout: (TextNodeLayout, () -> TextNode)?
|
||||
if let titleBadge = item.titleBadge {
|
||||
let titleBadgeTextNodeLayoutValue = makeTitleBadgeTextNodeLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
titleBadgeTextNodeLayout = titleBadgeTextNodeLayoutValue
|
||||
maxTitleWidth -= 5.0 + titleBadgeTextNodeLayoutValue.0.size.width
|
||||
}
|
||||
|
||||
let titleArguments = TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())
|
||||
let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil
|
||||
let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil
|
||||
|
|
@ -680,6 +716,41 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
|||
additionalDetailLabelNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let (badgeTextLayout, badgeTextApply) = titleBadgeTextNodeLayout {
|
||||
let titleBadgeNode: ASImageNode
|
||||
if let current = strongSelf.titleBadgeNode {
|
||||
titleBadgeNode = current
|
||||
} else {
|
||||
titleBadgeNode = ASImageNode()
|
||||
strongSelf.titleBadgeNode = titleBadgeNode
|
||||
strongSelf.addSubnode(titleBadgeNode)
|
||||
|
||||
titleBadgeNode.image = generateFilledRoundedRectImage(size: CGSize(width: 16.0, height: 16.0), cornerRadius: 5.0, color: item.presentationData.theme.list.itemCheckColors.fillColor)?.stretchableImage(withLeftCapWidth: 6, topCapHeight: 6)
|
||||
}
|
||||
|
||||
let titleBadgeTextNode = badgeTextApply()
|
||||
if titleBadgeTextNode.supernode == nil {
|
||||
strongSelf.addSubnode(titleBadgeTextNode)
|
||||
}
|
||||
let badgeSideInset: CGFloat = 5.0
|
||||
let badgeVerticalInset: CGFloat = 2.0
|
||||
let badgeSize = CGSize(width: badgeTextLayout.size.width + badgeSideInset * 2.0, height: badgeTextLayout.size.height + badgeVerticalInset * 2.0)
|
||||
let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 5.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)
|
||||
let titleBadgeTextFrame = CGRect(origin: CGPoint(x: titleBadgeFrame.minX + badgeSideInset, y: titleBadgeFrame.minY + badgeVerticalInset), size: badgeTextLayout.size)
|
||||
|
||||
titleBadgeNode.frame = titleBadgeFrame
|
||||
titleBadgeTextNode.frame = titleBadgeTextFrame
|
||||
} else {
|
||||
if let titleBadgeTextNode = strongSelf.titleBadgeTextNode {
|
||||
strongSelf.titleBadgeTextNode = nil
|
||||
titleBadgeTextNode.removeFromSupernode()
|
||||
}
|
||||
if let titleBadgeNode = strongSelf.titleBadgeNode {
|
||||
strongSelf.titleBadgeNode = nil
|
||||
titleBadgeNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let titleIcon = item.titleIcon {
|
||||
if strongSelf.titleIconNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.titleIconNode)
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@
|
|||
- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;
|
||||
- (void)clearPaintingData;
|
||||
|
||||
|
||||
- (bool)isCaptionAbove;
|
||||
- (SSignal *)captionAbove;
|
||||
- (void)setCaptionAbove:(bool)captionAbove;
|
||||
|
||||
|
||||
- (SSignal *)facesForItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
- (void)setFaces:(NSArray *)faces forItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation;
|
||||
@property (nonatomic, readonly) CGFloat keyboardHeight;
|
||||
@property (nonatomic, assign) CGFloat contentAreaHeight;
|
||||
@property (nonatomic, assign) UIEdgeInsets safeAreaInset;
|
||||
@property (nonatomic, assign) bool allowEntities;
|
||||
|
||||
@property (nonatomic, copy) UIView *(^panelParentView)(void);
|
||||
|
|
@ -23,9 +24,13 @@
|
|||
@property (nonatomic, copy) void (^finishedWithCaption)(NSAttributedString *caption);
|
||||
@property (nonatomic, copy) void (^keyboardHeightChanged)(CGFloat keyboardHeight, NSTimeInterval duration, NSInteger animationCurve);
|
||||
@property (nonatomic, copy) void (^timerUpdated)(NSNumber *timeout);
|
||||
@property (nonatomic, copy) void (^captionIsAboveUpdated)(bool captionIsAbove);
|
||||
|
||||
@property (nonatomic, readonly) bool editing;
|
||||
|
||||
- (void)createInputPanelIfNeeded;
|
||||
- (void)beginEditing;
|
||||
- (void)finishEditing;
|
||||
- (void)enableDismissal;
|
||||
|
||||
- (void)onAnimateOut;
|
||||
|
|
@ -36,7 +41,7 @@
|
|||
- (void)setCaption:(NSAttributedString *)caption animated:(bool)animated;
|
||||
- (void)setCaptionPanelHidden:(bool)hidden animated:(bool)animated;
|
||||
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo;
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo isCaptionAbove:(bool)isCaptionAbove;
|
||||
|
||||
- (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation;
|
||||
|
||||
@property (nonatomic, assign) CGFloat lowerBoundValue;
|
||||
@property (nonatomic, assign) UIColor *lowerBoundTrackColor;
|
||||
|
||||
@property (nonatomic, assign) CGFloat minimumValue;
|
||||
@property (nonatomic, assign) CGFloat maximumValue;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
@property (nonatomic, readonly) UIView * _Nonnull view;
|
||||
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo;
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo isCaptionAbove:(bool)isCaptionAbove;
|
||||
|
||||
- (NSAttributedString * _Nonnull)caption;
|
||||
- (void)setCaption:(NSAttributedString * _Nullable)caption;
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
@property (nonatomic, copy) void(^ _Nullable focusUpdated)(BOOL focused);
|
||||
@property (nonatomic, copy) void(^ _Nullable heightUpdated)(BOOL animated);
|
||||
@property (nonatomic, copy) void(^ _Nullable timerUpdated)(NSNumber * _Nullable value);
|
||||
@property (nonatomic, copy) void(^ _Nullable captionIsAboveUpdated)(BOOL value);
|
||||
|
||||
- (CGFloat)updateLayoutSize:(CGSize)size keyboardHeight:(CGFloat)keyboardHeight sideInset:(CGFloat)sideInset animated:(bool)animated;
|
||||
- (CGFloat)baseHeight;
|
||||
|
|
|
|||
|
|
@ -53,8 +53,9 @@
|
|||
sumOfWeights += 2.0 * standardGaussianWeights[currentGaussianWeightIndex];
|
||||
}
|
||||
|
||||
for (NSUInteger currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++)
|
||||
for (NSUInteger currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) {
|
||||
standardGaussianWeights[currentGaussianWeightIndex] = standardGaussianWeights[currentGaussianWeightIndex] / sumOfWeights;
|
||||
}
|
||||
|
||||
NSUInteger numberOfOptimizedOffsets = MIN(blurRadius / 2 + (blurRadius % 2), 7U);
|
||||
GLfloat *optimizedGaussianOffsets = calloc(numberOfOptimizedOffsets, sizeof(GLfloat));
|
||||
|
|
|
|||
|
|
@ -126,8 +126,11 @@
|
|||
SPipe *_pricePipe;
|
||||
SPipe *_fullSizePipe;
|
||||
SPipe *_cropPipe;
|
||||
SPipe *_captionAbovePipe;
|
||||
|
||||
NSAttributedString *_forcedCaption;
|
||||
|
||||
bool _captionAbove;
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
@ -196,6 +199,7 @@
|
|||
_pricePipe = [[SPipe alloc] init];
|
||||
_fullSizePipe = [[SPipe alloc] init];
|
||||
_cropPipe = [[SPipe alloc] init];
|
||||
_captionAbovePipe = [[SPipe alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
@ -853,6 +857,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (bool)isCaptionAbove {
|
||||
return _captionAbove;
|
||||
}
|
||||
|
||||
- (SSignal *)captionAbove
|
||||
{
|
||||
__weak TGMediaEditingContext *weakSelf = self;
|
||||
SSignal *updateSignal = [_captionAbovePipe.signalProducer() map:^NSNumber *(NSNumber *update)
|
||||
{
|
||||
__strong TGMediaEditingContext *strongSelf = weakSelf;
|
||||
return @(strongSelf->_captionAbove);
|
||||
}];
|
||||
|
||||
return [[SSignal single:@(_captionAbove)] then:updateSignal];
|
||||
}
|
||||
|
||||
- (void)setCaptionAbove:(bool)captionAbove
|
||||
{
|
||||
_captionAbove = captionAbove;
|
||||
_captionAbovePipe.sink(@(captionAbove));
|
||||
}
|
||||
|
||||
- (SSignal *)facesForItem:(NSObject<TGMediaEditableItem> *)item
|
||||
{
|
||||
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
|
||||
|
|
|
|||
|
|
@ -175,7 +175,15 @@
|
|||
[strongSelf.window endEditing:true];
|
||||
strongSelf->_portraitToolbarView.doneButton.userInteractionEnabled = false;
|
||||
strongSelf->_landscapeToolbarView.doneButton.userInteractionEnabled = false;
|
||||
strongSelf->_donePressed(strongSelf->_currentItem);
|
||||
|
||||
if (strongSelf->_captionMixin.editing) {
|
||||
[strongSelf->_captionMixin finishEditing];
|
||||
TGDispatchAfter(0.1, dispatch_get_main_queue(), ^{
|
||||
strongSelf->_donePressed(strongSelf->_currentItem);
|
||||
});
|
||||
} else {
|
||||
strongSelf->_donePressed(strongSelf->_currentItem);
|
||||
}
|
||||
|
||||
[strongSelf->_captionMixin onAnimateOut];
|
||||
};
|
||||
|
|
@ -389,6 +397,14 @@
|
|||
[strongSelf->_selectionContext setItem:(id<TGMediaSelectableItem>)galleryEditableItem.editableMediaItem selected:true animated:true sender:nil];
|
||||
};
|
||||
|
||||
_captionMixin.captionIsAboveUpdated = ^(bool captionIsAbove) {
|
||||
__strong TGMediaPickerGalleryInterfaceView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
[strongSelf->_editingContext setCaptionAbove:captionIsAbove];
|
||||
};
|
||||
|
||||
_captionMixin.stickersContext = stickersContext;
|
||||
[_captionMixin createInputPanelIfNeeded];
|
||||
|
||||
|
|
@ -818,6 +834,8 @@
|
|||
{
|
||||
id<TGMediaEditableItem> editableMediaItem = [galleryEditableItem editableMediaItem];
|
||||
|
||||
bool isCaptionAbove = galleryEditableItem.editingContext.isCaptionAbove;
|
||||
|
||||
__weak id<TGModernGalleryEditableItem> weakGalleryEditableItem = galleryEditableItem;
|
||||
[_adjustmentsDisposable setDisposable:[[[[galleryEditableItem.editingContext adjustmentsSignalForItem:editableMediaItem] mapToSignal:^SSignal *(id<TGMediaEditAdjustments> adjustments) {
|
||||
__strong id<TGModernGalleryEditableItem> strongGalleryEditableItem = weakGalleryEditableItem;
|
||||
|
|
@ -842,7 +860,7 @@
|
|||
id<TGMediaEditAdjustments> adjustments = dict[@"adjustments"];
|
||||
NSNumber *timer = dict[@"timer"];
|
||||
|
||||
[strongSelf->_captionMixin setTimeout:[timer intValue] isVideo:editableMediaItem.isVideo];
|
||||
[strongSelf->_captionMixin setTimeout:[timer intValue] isVideo:editableMediaItem.isVideo isCaptionAbove:isCaptionAbove];
|
||||
|
||||
if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]])
|
||||
{
|
||||
|
|
@ -1418,7 +1436,7 @@
|
|||
|| [view isDescendantOfView:_landscapeToolbarView]
|
||||
|| [view isDescendantOfView:_selectedPhotosView]
|
||||
|| [view isDescendantOfView:_captionMixin.inputPanelView]
|
||||
|| [view isDescendantOfView:_captionMixin.dismissView]
|
||||
|| ([view isDescendantOfView:_captionMixin.dismissView] && _captionMixin.dismissView.alpha > 0.0)
|
||||
|| [view isKindOfClass:[TGMenuButtonView class]])
|
||||
|
||||
{
|
||||
|
|
@ -1617,6 +1635,7 @@
|
|||
- (void)setSafeAreaInset:(UIEdgeInsets)safeAreaInset
|
||||
{
|
||||
_safeAreaInset = safeAreaInset;
|
||||
_captionMixin.safeAreaInset = safeAreaInset;
|
||||
[_currentItemView setSafeAreaInset:[self localSafeAreaInset]];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@
|
|||
|
||||
UIView *scrubberBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _headerView.frame.size.width, 64.0f)];
|
||||
scrubberBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
scrubberBackgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor];
|
||||
//scrubberBackgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor];
|
||||
[_scrubberPanelView addSubview:scrubberBackgroundView];
|
||||
|
||||
_scrubberView = [[TGMediaPickerGalleryVideoScrubber alloc] initWithFrame:CGRectMake(0.0f, _headerView.frame.size.height - 44.0f, _headerView.frame.size.width, 68.0f)];
|
||||
|
|
|
|||
|
|
@ -99,6 +99,12 @@ typedef enum
|
|||
_currentTimeLabel.backgroundColor = [UIColor clearColor];
|
||||
_currentTimeLabel.text = @"0:00";
|
||||
_currentTimeLabel.textColor = [UIColor whiteColor];
|
||||
_currentTimeLabel.layer.shadowOffset = CGSizeMake(0.0, 0.0);
|
||||
_currentTimeLabel.layer.shadowRadius = 4.0;
|
||||
_currentTimeLabel.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_currentTimeLabel.layer.shadowOpacity = 0.6;
|
||||
_currentTimeLabel.layer.rasterizationScale = TGScreenScaling();
|
||||
_currentTimeLabel.layer.shouldRasterize = true;
|
||||
[self addSubview:_currentTimeLabel];
|
||||
|
||||
_inverseTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 108, 4, 100, 15)];
|
||||
|
|
@ -108,6 +114,12 @@ typedef enum
|
|||
_inverseTimeLabel.text = @"0:00";
|
||||
_inverseTimeLabel.textAlignment = NSTextAlignmentRight;
|
||||
_inverseTimeLabel.textColor = [UIColor whiteColor];
|
||||
_inverseTimeLabel.layer.shadowOffset = CGSizeMake(0.0, 0.0);
|
||||
_inverseTimeLabel.layer.shadowRadius = 4.0;
|
||||
_inverseTimeLabel.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_inverseTimeLabel.layer.shadowOpacity = 0.6;
|
||||
_inverseTimeLabel.layer.rasterizationScale = TGScreenScaling();
|
||||
_inverseTimeLabel.layer.shouldRasterize = true;
|
||||
[self addSubview:_inverseTimeLabel];
|
||||
|
||||
_wrapperView = [[UIControl alloc] initWithFrame:CGRectMake(8, 24, 0, 36)];
|
||||
|
|
@ -119,14 +131,19 @@ typedef enum
|
|||
|
||||
_summaryThumbnailWrapperView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 32)];
|
||||
_summaryThumbnailWrapperView.clipsToBounds = true;
|
||||
_summaryThumbnailWrapperView.layer.cornerRadius = 5.0;
|
||||
[_wrapperView addSubview:_summaryThumbnailWrapperView];
|
||||
|
||||
_leftCurtainView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
|
||||
_leftCurtainView.backgroundColor = [[TGPhotoEditorInterfaceAssets toolbarBackgroundColor] colorWithAlphaComponent:0.8f];
|
||||
_leftCurtainView.clipsToBounds = true;
|
||||
_leftCurtainView.layer.cornerRadius = 5.0;
|
||||
[_wrapperView addSubview:_leftCurtainView];
|
||||
|
||||
_rightCurtainView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
|
||||
_rightCurtainView.backgroundColor = [[TGPhotoEditorInterfaceAssets toolbarBackgroundColor] colorWithAlphaComponent:0.8f];
|
||||
_rightCurtainView.clipsToBounds = true;
|
||||
_rightCurtainView.layer.cornerRadius = 5.0;
|
||||
[_wrapperView addSubview:_rightCurtainView];
|
||||
|
||||
__weak TGMediaPickerGalleryVideoScrubber *weakSelf = self;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
CGRect _currentFrame;
|
||||
UIEdgeInsets _currentEdgeInsets;
|
||||
|
||||
bool _currentIsCaptionAbove;
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
@ -59,7 +61,6 @@
|
|||
_inputPanel.sendPressed = ^(NSAttributedString *string) {
|
||||
__strong TGPhotoCaptionInputMixin *strongSelf = weakSelf;
|
||||
[TGViewController enableAutorotation];
|
||||
strongSelf->_dismissView.hidden = true;
|
||||
|
||||
strongSelf->_editing = false;
|
||||
|
||||
|
|
@ -72,9 +73,7 @@
|
|||
[TGViewController disableAutorotation];
|
||||
|
||||
[strongSelf beginEditing];
|
||||
|
||||
strongSelf->_dismissView.hidden = false;
|
||||
|
||||
|
||||
if (strongSelf.panelFocused != nil)
|
||||
strongSelf.panelFocused();
|
||||
|
||||
|
|
@ -94,12 +93,21 @@
|
|||
}
|
||||
};
|
||||
|
||||
_inputPanel.captionIsAboveUpdated = ^(bool value) {
|
||||
__strong TGPhotoCaptionInputMixin *strongSelf = weakSelf;
|
||||
if (strongSelf.captionIsAboveUpdated != nil) {
|
||||
strongSelf.captionIsAboveUpdated(value);
|
||||
|
||||
strongSelf->_currentIsCaptionAbove = value;
|
||||
[strongSelf updateLayoutWithFrame:strongSelf->_currentFrame edgeInsets:strongSelf->_currentEdgeInsets animated:true];
|
||||
}
|
||||
};
|
||||
|
||||
_inputPanelView = inputPanel.view;
|
||||
|
||||
_backgroundView = [[UIView alloc] init];
|
||||
_backgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor];
|
||||
[parentView addSubview:_backgroundView];
|
||||
//[parentView addSubview:_backgroundView];
|
||||
[parentView addSubview:_inputPanelView];
|
||||
}
|
||||
|
||||
|
|
@ -118,12 +126,13 @@
|
|||
|
||||
_dismissView = [[UIView alloc] initWithFrame:parentView.bounds];
|
||||
_dismissView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_dismissView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||
|
||||
_dismissTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissTap:)];
|
||||
_dismissTapRecognizer.enabled = false;
|
||||
[_dismissView addGestureRecognizer:_dismissTapRecognizer];
|
||||
|
||||
[parentView insertSubview:_dismissView belowSubview:_backgroundView];
|
||||
[parentView insertSubview:_dismissView belowSubview:_inputPanelView];
|
||||
}
|
||||
|
||||
- (void)setCaption:(NSAttributedString *)caption
|
||||
|
|
@ -141,8 +150,9 @@
|
|||
[_inputPanel setCaption:caption];
|
||||
}
|
||||
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo {
|
||||
[_inputPanel setTimeout:timeout isVideo:isVideo];
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo isCaptionAbove:(bool)isCaptionAbove {
|
||||
_currentIsCaptionAbove = isCaptionAbove;
|
||||
[_inputPanel setTimeout:timeout isVideo:isVideo isCaptionAbove:isCaptionAbove];
|
||||
}
|
||||
|
||||
- (void)setCaptionPanelHidden:(bool)hidden animated:(bool)__unused animated
|
||||
|
|
@ -156,6 +166,12 @@
|
|||
|
||||
[self createDismissViewIfNeeded];
|
||||
[self createInputPanelIfNeeded];
|
||||
|
||||
_dismissView.alpha = 0.0;
|
||||
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
_dismissView.alpha = 1.0f;
|
||||
} completion:^(BOOL finished) {
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)enableDismissal
|
||||
|
|
@ -165,19 +181,21 @@
|
|||
|
||||
#pragma mark -
|
||||
|
||||
- (void)finishEditing {
|
||||
if ([self.inputPanel dismissInput]) {
|
||||
_editing = false;
|
||||
|
||||
if (self.finishedWithCaption != nil)
|
||||
self.finishedWithCaption([_inputPanel caption]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleDismissTap:(UITapGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state != UIGestureRecognizerStateRecognized)
|
||||
return;
|
||||
|
||||
if ([self.inputPanel dismissInput]) {
|
||||
_editing = false;
|
||||
|
||||
[_dismissView removeFromSuperview];
|
||||
|
||||
if (self.finishedWithCaption != nil)
|
||||
self.finishedWithCaption([_inputPanel caption]);
|
||||
}
|
||||
[self finishEditing];
|
||||
}
|
||||
|
||||
#pragma mark - Input Panel Delegate
|
||||
|
|
@ -216,20 +234,49 @@
|
|||
|
||||
_keyboardHeight = keyboardHeight;
|
||||
|
||||
CGFloat fadeAlpha = 1.0;
|
||||
if (keyboardHeight < FLT_EPSILON) {
|
||||
fadeAlpha = 0.0;
|
||||
}
|
||||
|
||||
if (ABS(_dismissView.alpha - fadeAlpha) > FLT_EPSILON) {
|
||||
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
_dismissView.alpha = fadeAlpha;
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
}];
|
||||
}
|
||||
|
||||
if (!UIInterfaceOrientationIsPortrait([[LegacyComponentsGlobals provider] applicationStatusBarOrientation]) && !TGIsPad())
|
||||
return;
|
||||
|
||||
CGRect frame = _currentFrame;
|
||||
UIEdgeInsets edgeInsets = _currentEdgeInsets;
|
||||
CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:false];
|
||||
[UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{
|
||||
_inputPanelView.frame = CGRectMake(edgeInsets.left, frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight), frame.size.width, panelHeight);
|
||||
|
||||
CGFloat backgroundHeight = panelHeight;
|
||||
if (_keyboardHeight > 0.0) {
|
||||
backgroundHeight += _keyboardHeight - edgeInsets.bottom;
|
||||
|
||||
CGFloat panelY = 0.0;
|
||||
if (frame.size.width > frame.size.height && !TGIsPad()) {
|
||||
panelY = edgeInsets.top + frame.size.height;
|
||||
} else {
|
||||
if (_currentIsCaptionAbove) {
|
||||
if (_keyboardHeight > 0.0) {
|
||||
panelY = _safeAreaInset.top + 8.0;
|
||||
} else {
|
||||
panelY = _safeAreaInset.top + 8.0 + 40.0;
|
||||
}
|
||||
} else {
|
||||
panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight);
|
||||
}
|
||||
_backgroundView.frame = CGRectMake(edgeInsets.left, frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight), frame.size.width, backgroundHeight);
|
||||
}
|
||||
|
||||
CGFloat backgroundHeight = panelHeight;
|
||||
if (_keyboardHeight > 0.0) {
|
||||
backgroundHeight += _keyboardHeight - edgeInsets.bottom;
|
||||
}
|
||||
|
||||
[UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{
|
||||
_inputPanelView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight);
|
||||
_backgroundView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, backgroundHeight);
|
||||
} completion:nil];
|
||||
|
||||
if (self.keyboardHeightChanged != nil)
|
||||
|
|
@ -243,11 +290,19 @@
|
|||
|
||||
CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:_keyboardHeight sideInset:0.0 animated:animated];
|
||||
|
||||
CGFloat y = 0.0;
|
||||
CGFloat panelY = 0.0;
|
||||
if (frame.size.width > frame.size.height && !TGIsPad()) {
|
||||
y = edgeInsets.top + frame.size.height;
|
||||
panelY = edgeInsets.top + frame.size.height;
|
||||
} else {
|
||||
y = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight);
|
||||
if (_currentIsCaptionAbove) {
|
||||
if (_keyboardHeight > 0.0) {
|
||||
panelY = _safeAreaInset.top + 8.0;
|
||||
} else {
|
||||
panelY = _safeAreaInset.top + 8.0 + 40.0;
|
||||
}
|
||||
} else {
|
||||
panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight);
|
||||
}
|
||||
}
|
||||
|
||||
CGFloat backgroundHeight = panelHeight;
|
||||
|
|
@ -255,8 +310,8 @@
|
|||
backgroundHeight += _keyboardHeight - edgeInsets.bottom;
|
||||
}
|
||||
|
||||
CGRect panelFrame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight);
|
||||
CGRect backgroundFrame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight);
|
||||
CGRect panelFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight);
|
||||
CGRect backgroundFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, backgroundHeight);
|
||||
|
||||
if (animated) {
|
||||
[_inputPanel animateView:_inputPanelView frame:panelFrame];
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
|
|||
CGFloat knobPosition = _knobPadding + (_knobView.highlighted ? _knobDragCenter : [self centerPositionForValue:_value totalLength:totalLength knobSize:_knobView.image.size.width vertical:vertical]);
|
||||
knobPosition = MAX(_knobPadding, MIN(knobPosition, _knobPadding + totalLength));
|
||||
|
||||
CGFloat lowerBoundPosition = _knobPadding + [self centerPositionForValue:_lowerBoundValue totalLength:totalLength knobSize:_knobView.image.size.width vertical:vertical];
|
||||
|
||||
CGFloat startPosition = visualMargin + visualTotalLength / (_maximumValue - _minimumValue) * (ABS(_minimumValue) + _startValue);
|
||||
if (vertical)
|
||||
startPosition = 2 * visualMargin + visualTotalLength - startPosition;
|
||||
|
|
@ -181,88 +183,120 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
|
|||
CGContextSetBlendMode(context, kCGBlendModeCopy);
|
||||
}
|
||||
|
||||
CGContextSetFillColorWithColor(context, _backColor.CGColor);
|
||||
[self drawRectangle:backFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
|
||||
CGContextSetBlendMode(context, kCGBlendModeNormal);
|
||||
|
||||
CGContextSetFillColorWithColor(context, _trackColor.CGColor);
|
||||
[self drawRectangle:trackFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
|
||||
if (!_startHidden || self.displayEdges)
|
||||
{
|
||||
bool highlighted = CGRectGetMidX(startFrame) < CGRectGetMaxX(trackFrame);
|
||||
if (vertical)
|
||||
highlighted = CGRectGetMidY(startFrame) > CGRectGetMinY(trackFrame);
|
||||
highlighted = highlighted && self.displayEdges;
|
||||
for (int passIndex = 0; passIndex < 2; passIndex++) {
|
||||
CGContextSaveGState(context);
|
||||
CGContextResetClip(context);
|
||||
|
||||
CGContextSetFillColorWithColor(context, highlighted ? _trackColor.CGColor : _startColor.CGColor);
|
||||
[self drawRectangle:startFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
}
|
||||
|
||||
if (self.displayEdges) {
|
||||
CGContextSetFillColorWithColor(context, _backColor.CGColor);
|
||||
[self drawRectangle:endFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
}
|
||||
|
||||
if (_bordered)
|
||||
{
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.6f).CGColor);
|
||||
CGContextFillEllipseInRect(context, CGRectInset(knobFrame, 1.0f, 1.0f));
|
||||
}
|
||||
|
||||
if (self.positionsCount > 1)
|
||||
{
|
||||
for (NSInteger i = 0; i < self.positionsCount; i++)
|
||||
{
|
||||
if (!self.markPositions) {
|
||||
if (i != 0 && i != self.positionsCount - 1) {
|
||||
continue;
|
||||
}
|
||||
UIColor *passBackColor = _backColor;
|
||||
UIColor *passTrackColor = _trackColor;
|
||||
|
||||
if (passIndex == 0) {
|
||||
if (_lowerBoundValue > 0.0f && _lowerBoundTrackColor != nil) {
|
||||
CGContextBeginPath(context);
|
||||
CGContextAddRect(context, CGRectMake(0.0, 0.0, lowerBoundPosition, rect.size.height));
|
||||
CGContextClip(context);
|
||||
|
||||
CGFloat trackAlpha = 0.0f;
|
||||
[_trackColor getRed:nil green:nil blue:nil alpha:&trackAlpha];
|
||||
|
||||
passTrackColor = _lowerBoundTrackColor;
|
||||
}
|
||||
|
||||
if (self.useLinesForPositions) {
|
||||
CGSize lineSize = CGSizeMake(4.0, 12.0);
|
||||
CGRect lineRect = CGRectMake(margin - lineSize.width / 2.0f + totalLength / (self.positionsCount - 1) * i, (sideLength - lineSize.height) / 2, lineSize.width, lineSize.height);
|
||||
if (vertical)
|
||||
lineRect = CGRectMake(lineRect.origin.y, lineRect.origin.x, lineRect.size.height, lineRect.size.width);
|
||||
|
||||
bool highlighted = CGRectGetMidX(lineRect) < CGRectGetMaxX(trackFrame);
|
||||
if (vertical)
|
||||
highlighted = CGRectGetMidY(lineRect) > CGRectGetMinY(trackFrame);
|
||||
|
||||
CGContextSetFillColorWithColor(context, highlighted ? _trackColor.CGColor : _backColor.CGColor);
|
||||
[self drawRectangle:lineRect cornerRadius:self.trackCornerRadius context:context];
|
||||
} else {
|
||||
if (_lowerBoundValue > 0.0f && _lowerBoundTrackColor != nil && lowerBoundPosition < rect.size.width) {
|
||||
CGContextBeginPath(context);
|
||||
CGContextAddRect(context, CGRectMake(lowerBoundPosition, 0.0, rect.size.width - lowerBoundPosition, rect.size.height));
|
||||
CGContextClip(context);
|
||||
} else {
|
||||
if ([self.backgroundColor isEqual:[UIColor clearColor]])
|
||||
{
|
||||
CGContextSetBlendMode(context, kCGBlendModeClear);
|
||||
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
|
||||
}
|
||||
|
||||
CGFloat inset = 1.5f;
|
||||
CGFloat outerSize = _dotSize + inset * 2.0f;
|
||||
CGRect dotRect = CGRectMake(margin - outerSize / 2.0f + totalLength / (self.positionsCount - 1) * i, (sideLength - outerSize) / 2, outerSize, outerSize);
|
||||
if (vertical)
|
||||
dotRect = CGRectMake(dotRect.origin.y, dotRect.origin.x, dotRect.size.height, dotRect.size.width);
|
||||
|
||||
CGContextFillEllipseInRect(context, dotRect);
|
||||
|
||||
dotRect = CGRectInset(dotRect, inset, inset);
|
||||
|
||||
CGContextSetBlendMode(context, kCGBlendModeNormal);
|
||||
bool highlighted = CGRectGetMidX(dotRect) < CGRectGetMaxX(trackFrame);
|
||||
if (vertical)
|
||||
highlighted = CGRectGetMidY(dotRect) > CGRectGetMinY(trackFrame);
|
||||
|
||||
CGContextSetFillColorWithColor(context, highlighted ? _trackColor.CGColor : _backColor.CGColor);
|
||||
CGContextFillEllipseInRect(context, dotRect);
|
||||
CGContextRestoreGState(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CGContextSetFillColorWithColor(context, passBackColor.CGColor);
|
||||
[self drawRectangle:backFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
|
||||
CGContextSetBlendMode(context, kCGBlendModeNormal);
|
||||
|
||||
CGContextSetFillColorWithColor(context, passTrackColor.CGColor);
|
||||
[self drawRectangle:trackFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
|
||||
if (!_startHidden || self.displayEdges)
|
||||
{
|
||||
bool highlighted = CGRectGetMidX(startFrame) < CGRectGetMaxX(trackFrame);
|
||||
if (vertical)
|
||||
highlighted = CGRectGetMidY(startFrame) > CGRectGetMinY(trackFrame);
|
||||
highlighted = highlighted && self.displayEdges;
|
||||
|
||||
CGContextSetFillColorWithColor(context, highlighted ? passTrackColor.CGColor : _startColor.CGColor);
|
||||
[self drawRectangle:startFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
}
|
||||
|
||||
if (self.displayEdges) {
|
||||
CGContextSetFillColorWithColor(context, passBackColor.CGColor);
|
||||
[self drawRectangle:endFrame cornerRadius:self.trackCornerRadius context:context];
|
||||
}
|
||||
|
||||
if (_bordered)
|
||||
{
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.6f).CGColor);
|
||||
CGContextFillEllipseInRect(context, CGRectInset(knobFrame, 1.0f, 1.0f));
|
||||
}
|
||||
|
||||
if (self.positionsCount > 1)
|
||||
{
|
||||
for (NSInteger i = 0; i < self.positionsCount; i++)
|
||||
{
|
||||
if (!self.markPositions) {
|
||||
if (i != 0 && i != self.positionsCount - 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.useLinesForPositions) {
|
||||
CGSize lineSize = CGSizeMake(4.0, 12.0);
|
||||
CGRect lineRect = CGRectMake(margin - lineSize.width / 2.0f + totalLength / (self.positionsCount - 1) * i, (sideLength - lineSize.height) / 2, lineSize.width, lineSize.height);
|
||||
if (vertical)
|
||||
lineRect = CGRectMake(lineRect.origin.y, lineRect.origin.x, lineRect.size.height, lineRect.size.width);
|
||||
|
||||
bool highlighted = CGRectGetMidX(lineRect) < CGRectGetMaxX(trackFrame);
|
||||
if (vertical)
|
||||
highlighted = CGRectGetMidY(lineRect) > CGRectGetMinY(trackFrame);
|
||||
|
||||
CGContextSetFillColorWithColor(context, highlighted ? passTrackColor.CGColor : passBackColor.CGColor);
|
||||
[self drawRectangle:lineRect cornerRadius:self.trackCornerRadius context:context];
|
||||
} else {
|
||||
if ([self.backgroundColor isEqual:[UIColor clearColor]])
|
||||
{
|
||||
CGContextSetBlendMode(context, kCGBlendModeClear);
|
||||
CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
|
||||
}
|
||||
|
||||
CGFloat inset = 1.5f;
|
||||
CGFloat outerSize = _dotSize + inset * 2.0f;
|
||||
CGRect dotRect = CGRectMake(margin - outerSize / 2.0f + totalLength / (self.positionsCount - 1) * i, (sideLength - outerSize) / 2, outerSize, outerSize);
|
||||
if (vertical)
|
||||
dotRect = CGRectMake(dotRect.origin.y, dotRect.origin.x, dotRect.size.height, dotRect.size.width);
|
||||
|
||||
CGContextFillEllipseInRect(context, dotRect);
|
||||
|
||||
dotRect = CGRectInset(dotRect, inset, inset);
|
||||
|
||||
CGContextSetBlendMode(context, kCGBlendModeNormal);
|
||||
bool highlighted = CGRectGetMidX(dotRect) < CGRectGetMaxX(trackFrame);
|
||||
if (vertical)
|
||||
highlighted = CGRectGetMidY(dotRect) > CGRectGetMinY(trackFrame);
|
||||
|
||||
CGContextSetFillColorWithColor(context, highlighted ? passTrackColor.CGColor : passBackColor.CGColor);
|
||||
CGContextFillEllipseInRect(context, dotRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CGContextRestoreGState(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,7 +381,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
|
|||
|
||||
- (void)setValue:(CGFloat)value animated:(BOOL)__unused animated
|
||||
{
|
||||
_value = MIN(MAX(value, _minimumValue), _maximumValue);
|
||||
_value = MIN(MAX(_lowerBoundValue, MAX(value, _minimumValue)), _maximumValue);
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
|
|
@ -669,7 +703,16 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f;
|
|||
if (self.positionsCount > 1 && !self.disableSnapToPositions)
|
||||
{
|
||||
NSInteger position = (NSInteger)round((_knobDragCenter / totalLength) * (self.positionsCount - 1));
|
||||
|
||||
if (_lowerBoundValue > 0.0f) {
|
||||
position = MAX(position, (NSInteger)_lowerBoundValue);
|
||||
}
|
||||
|
||||
_knobDragCenter = position * totalLength / (self.positionsCount - 1);
|
||||
} else {
|
||||
if (_lowerBoundValue > 0.0f) {
|
||||
_knobDragCenter = MAX(_knobDragCenter, _lowerBoundValue * totalLength);
|
||||
}
|
||||
}
|
||||
|
||||
[self setValue:[self valueForCenterPosition:_knobDragCenter totalLength:totalLength knobSize:_knobView.image.size.width vertical:vertical]];
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ public func legacyStoryMediaEditor(context: AccountContext, item: TGMediaEditabl
|
|||
})
|
||||
}
|
||||
|
||||
public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, mode: LegacyMediaEditorMode, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, mode: LegacyMediaEditorMode, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media)
|
||||
|> deliverOnMainQueue).start(next: { (value, isImage) in
|
||||
guard case let .data(data) = value, data.complete else {
|
||||
|
|
@ -215,7 +215,8 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle:
|
|||
let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: selectableResult, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3 in
|
||||
nativeGenerator(_1, _2, _3, nil)
|
||||
})
|
||||
sendMessagesWithSignals(signals, false, 0)
|
||||
let isCaptionAbove = editingContext?.isCaptionAbove() ?? false
|
||||
sendMessagesWithSignals(signals, false, 0, isCaptionAbove)
|
||||
}, dismissed: { [weak legacyController] in
|
||||
legacyController?.dismiss()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public func nearbyVenues(context: AccountContext, story: Bool = false, latitude:
|
|||
}
|
||||
return botUsername
|
||||
|> mapToSignal { botUsername in
|
||||
return context.engine.peers.resolvePeerByName(name: botUsername)
|
||||
return context.engine.peers.resolvePeerByName(name: botUsername, referrer: nil)
|
||||
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ public func mediaPasteboardScreen(
|
|||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
peer: EnginePeer,
|
||||
subjects: [MediaPickerScreen.Subject.Media],
|
||||
presentMediaPicker: @escaping (_ subject: MediaPickerScreen.Subject, _ saveEditedPhotos: Bool, _ bannedSendPhotos: (Int32, Bool)?, _ bannedSendVideos: (Int32, Bool)?, _ present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void) -> Void,
|
||||
subjects: [MediaPickerScreenImpl.Subject.Media],
|
||||
presentMediaPicker: @escaping (_ subject: MediaPickerScreenImpl.Subject, _ saveEditedPhotos: Bool, _ bannedSendPhotos: (Int32, Bool)?, _ bannedSendVideos: (Int32, Bool)?, _ present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void) -> Void,
|
||||
getSourceRect: (() -> CGRect?)? = nil,
|
||||
makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil }
|
||||
) -> ViewController {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ swift_library(
|
|||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
"//submodules/TelegramUI/Components/MediaAssetsContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import LegacyComponents
|
|||
import LegacyUI
|
||||
import LegacyMediaPickerUI
|
||||
import Photos
|
||||
import MediaAssetsContext
|
||||
|
||||
private func galleryFetchResultItems(fetchResult: PHFetchResult<PHAsset>, index: Int, reversed: Bool, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, stickersContext: TGPhotoPaintStickersContext, immediateThumbnail: UIImage?) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
|
||||
var focusItem: TGModernGalleryItem?
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import TelegramPresentationData
|
|||
import ItemListUI
|
||||
import MergeLists
|
||||
import Photos
|
||||
import MediaAssetsContext
|
||||
|
||||
private struct MediaGroupsGridAlbumEntry: Comparable, Identifiable {
|
||||
let theme: PresentationTheme
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import ContextUI
|
|||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import Photos
|
||||
import MediaAssetsContext
|
||||
|
||||
struct MediaGroupItem {
|
||||
let collection: PHAssetCollection
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import Photos
|
|||
import LegacyComponents
|
||||
import AttachmentUI
|
||||
import ItemListUI
|
||||
import CameraScreen
|
||||
import MediaAssetsContext
|
||||
|
||||
private enum MediaGroupsEntry: Comparable, Identifiable {
|
||||
enum StableId: Hashable {
|
||||
|
|
@ -470,7 +470,7 @@ public final class MediaGroupsScreen: ViewController, AttachmentContainable {
|
|||
} else {
|
||||
self.updateNavigationStack { current in
|
||||
var mediaPickerContext: AttachmentMediaPickerContext?
|
||||
if let first = current.first as? MediaPickerScreen {
|
||||
if let first = current.first as? MediaPickerScreenImpl {
|
||||
mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext
|
||||
}
|
||||
return (current.filter { $0 !== self }, mediaPickerContext)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import ImageBlur
|
|||
import FastBlur
|
||||
import MediaEditor
|
||||
import RadialStatusNode
|
||||
import MediaAssetsContext
|
||||
|
||||
private let leftShadowImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
|
||||
|
|
@ -48,7 +49,7 @@ private let rightShadowImage: UIImage = {
|
|||
|
||||
enum MediaPickerGridItemContent: Equatable {
|
||||
case asset(PHFetchResult<PHAsset>, Int)
|
||||
case media(MediaPickerScreen.Subject.Media, Int)
|
||||
case media(MediaPickerScreenImpl.Subject.Media, Int)
|
||||
case draft(MediaEditorDraft, Int)
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +396,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
self.updateHiddenMedia()
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreenImpl.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import MediaEditor
|
|||
import ImageObjectSeparation
|
||||
import ChatSendMessageActionUI
|
||||
import AnimatedCountLabelNode
|
||||
import MediaAssetsContext
|
||||
|
||||
final class MediaPickerInteraction {
|
||||
let downloadManager: AssetDownloadManager
|
||||
|
|
@ -40,16 +41,7 @@ final class MediaPickerInteraction {
|
|||
let selectionState: TGMediaSelectionContext?
|
||||
let editingState: TGMediaEditingContext
|
||||
var hiddenMediaId: String?
|
||||
|
||||
var captionIsAboveMedia: Bool = false {
|
||||
didSet {
|
||||
if self.captionIsAboveMedia != oldValue {
|
||||
self.captionIsAboveMediaValue.set(self.captionIsAboveMedia)
|
||||
}
|
||||
}
|
||||
}
|
||||
let captionIsAboveMediaValue = ValuePromise<Bool>(false)
|
||||
|
||||
|
||||
init(downloadManager: AssetDownloadManager, openMedia: @escaping (PHFetchResult<PHAsset>, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, openDraft: @escaping (MediaEditorDraft, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Bool, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, ChatSendMessageActionSheetController.SendParameters?, @escaping () -> Void) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) {
|
||||
self.downloadManager = downloadManager
|
||||
self.openMedia = openMedia
|
||||
|
|
@ -138,7 +130,7 @@ struct Month: Equatable {
|
|||
|
||||
private var savedStoriesContentOffset: CGFloat?
|
||||
|
||||
public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, AttachmentContainable {
|
||||
public enum Subject {
|
||||
public enum Media: Equatable {
|
||||
case image(UIImage)
|
||||
|
|
@ -207,7 +199,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
|
||||
public var openBoost: () -> Void = { }
|
||||
|
||||
public var customSelection: ((MediaPickerScreen, Any) -> Void)? = nil
|
||||
public var customSelection: ((MediaPickerScreenImpl, Any) -> Void)? = nil
|
||||
|
||||
public var createFromScratch: () -> Void = {}
|
||||
public var presentFilePicker: () -> Void = {}
|
||||
|
|
@ -250,7 +242,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
case media([Subject.Media])
|
||||
}
|
||||
|
||||
private weak var controller: MediaPickerScreen?
|
||||
private weak var controller: MediaPickerScreenImpl?
|
||||
private var presentationData: PresentationData
|
||||
fileprivate let mediaAssetsContext: MediaAssetsContext
|
||||
|
||||
|
|
@ -307,7 +299,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
init(controller: MediaPickerScreen) {
|
||||
init(controller: MediaPickerScreenImpl) {
|
||||
self.controller = controller
|
||||
self.presentationData = controller.presentationData
|
||||
|
||||
|
|
@ -1255,7 +1247,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
if parameters == nil {
|
||||
var textIsAboveMedia = false
|
||||
if let interaction = controller.interaction {
|
||||
textIsAboveMedia = interaction.captionIsAboveMedia
|
||||
textIsAboveMedia = interaction.editingState.isCaptionAbove()
|
||||
}
|
||||
parameters = ChatSendMessageActionSheetController.SendParameters(
|
||||
effect: nil,
|
||||
|
|
@ -2311,7 +2303,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
} else {
|
||||
self.updateNavigationStack { current in
|
||||
var mediaPickerContext: AttachmentMediaPickerContext?
|
||||
if let first = current.first as? MediaPickerScreen {
|
||||
if let first = current.first as? MediaPickerScreenImpl {
|
||||
mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext
|
||||
}
|
||||
return (current.filter { $0 !== self }, mediaPickerContext)
|
||||
|
|
@ -2417,7 +2409,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
var updateNavigationStackImpl: ((AttachmentContainable) -> Void)?
|
||||
let groupsController = MediaGroupsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mediaAssetsContext: self.controllerNode.mediaAssetsContext, embedded: embedded, openGroup: { [weak self] collection in
|
||||
if let strongSelf = self {
|
||||
let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, isScheduledMessages: strongSelf.isScheduledMessages, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
|
||||
let mediaPicker = MediaPickerScreenImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, isScheduledMessages: strongSelf.isScheduledMessages, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
|
||||
|
||||
mediaPicker.presentSchedulePicker = strongSelf.presentSchedulePicker
|
||||
mediaPicker.presentTimerPicker = strongSelf.presentTimerPicker
|
||||
|
|
@ -2576,7 +2568,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
if isCaptionAboveMediaAvailable {
|
||||
var mediaCaptionIsAbove = false
|
||||
if let interaction = self.interaction {
|
||||
mediaCaptionIsAbove = interaction.captionIsAboveMedia
|
||||
mediaCaptionIsAbove = interaction.editingState.isCaptionAbove()
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: mediaCaptionIsAbove ? strings.Chat_SendMessageMenu_MoveCaptionDown : strings.Chat_SendMessageMenu_MoveCaptionUp, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation(
|
||||
|
|
@ -2588,7 +2580,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
}
|
||||
|
||||
if let interaction = strongSelf.interaction {
|
||||
interaction.captionIsAboveMedia = !interaction.captionIsAboveMedia
|
||||
interaction.editingState.setCaptionAbove(!interaction.editingState.isCaptionAbove())
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
|
@ -2684,13 +2676,17 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - self.selectedButtonNode.frame.width - safeInset, y: floorToScreenPixels((navigationHeight - self.selectedButtonNode.frame.height) / 2.0) + 1.0), size: self.selectedButtonNode.frame.size)
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
self.requestDismiss(completion: {})
|
||||
}
|
||||
|
||||
public var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||
return MediaPickerContext(controller: self)
|
||||
}
|
||||
}
|
||||
|
||||
final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||
private weak var controller: MediaPickerScreen?
|
||||
private weak var controller: MediaPickerScreenImpl?
|
||||
|
||||
var selectionCount: Signal<Int, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
|
|
@ -2791,23 +2787,32 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
|||
|
||||
var captionIsAboveMedia: Signal<Bool, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
guard let interaction = self?.controller?.interaction else {
|
||||
guard let self else {
|
||||
subscriber.putNext(false)
|
||||
subscriber.putCompletion()
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
let disposable = interaction.captionIsAboveMediaValue.get().start(next: { value in
|
||||
subscriber.putNext(value)
|
||||
guard let captionAbove = self.controller?.interaction?.editingState.captionAbove() else {
|
||||
subscriber.putNext(false)
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let disposable = captionAbove.start(next: { caption in
|
||||
if let caption = caption as? NSNumber {
|
||||
subscriber.putNext(caption.boolValue)
|
||||
} else {
|
||||
subscriber.putNext(false)
|
||||
}
|
||||
}, error: { _ in }, completed: { })
|
||||
return ActionDisposable {
|
||||
disposable.dispose()
|
||||
disposable?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void {
|
||||
self.controller?.interaction?.captionIsAboveMedia = captionIsAboveMedia
|
||||
self.controller?.interaction?.editingState.setCaptionAbove(captionIsAboveMedia)
|
||||
}
|
||||
|
||||
public var loadingProgress: Signal<CGFloat?, NoError> {
|
||||
|
|
@ -2818,7 +2823,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
|||
return .single(self.controller?.mainButtonState)
|
||||
}
|
||||
|
||||
init(controller: MediaPickerScreen) {
|
||||
init(controller: MediaPickerScreenImpl) {
|
||||
self.controller = controller
|
||||
}
|
||||
|
||||
|
|
@ -2954,7 +2959,7 @@ public func wallpaperMediaPickerController(
|
|||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
peer: EnginePeer,
|
||||
animateAppearance: Bool,
|
||||
completion: @escaping (MediaPickerScreen, Any) -> Void = { _, _ in },
|
||||
completion: @escaping (MediaPickerScreenImpl, Any) -> Void = { _, _ in },
|
||||
openColors: @escaping () -> Void
|
||||
) -> ViewController {
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
|
||||
|
|
@ -2963,7 +2968,7 @@ public func wallpaperMediaPickerController(
|
|||
controller.animateAppearance = animateAppearance
|
||||
controller.requestController = { [weak controller] _, present in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper), mainButtonState: AttachmentMainButtonState(text: presentationData.strings.Conversation_Theme_SetColorWallpaper, font: .regular, background: .color(.clear), textColor: presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false), mainButtonAction: {
|
||||
let mediaPickerController = MediaPickerScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper), mainButtonState: AttachmentMainButtonState(text: presentationData.strings.Conversation_Theme_SetColorWallpaper, font: .regular, background: .color(.clear), textColor: presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false), mainButtonAction: {
|
||||
controller?.dismiss(animated: true)
|
||||
openColors()
|
||||
})
|
||||
|
|
@ -2985,7 +2990,7 @@ public func mediaPickerController(
|
|||
return nil
|
||||
})
|
||||
controller.requestController = { _, present in
|
||||
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .addImage), mainButtonState: nil, mainButtonAction: nil)
|
||||
let mediaPickerController = MediaPickerScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .addImage), mainButtonState: nil, mainButtonAction: nil)
|
||||
mediaPickerController.customSelection = { controller, result in
|
||||
completion(result)
|
||||
controller.dismiss(animated: true)
|
||||
|
|
@ -3020,6 +3025,7 @@ public func mediaPickerController(
|
|||
public func storyMediaPickerController(
|
||||
context: AccountContext,
|
||||
isDark: Bool,
|
||||
forCollage: Bool,
|
||||
getSourceRect: @escaping () -> CGRect,
|
||||
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
|
||||
dismissed: @escaping () -> Void,
|
||||
|
|
@ -3036,7 +3042,7 @@ public func storyMediaPickerController(
|
|||
controller.forceSourceRect = true
|
||||
controller.getSourceRect = getSourceRect
|
||||
controller.requestController = { _, present in
|
||||
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil)
|
||||
let mediaPickerController = MediaPickerScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil)
|
||||
mediaPickerController.groupsPresented = groupsPresented
|
||||
mediaPickerController.customSelection = { controller, result in
|
||||
if let result = result as? MediaEditorDraft {
|
||||
|
|
@ -3062,7 +3068,9 @@ public func storyMediaPickerController(
|
|||
})
|
||||
}
|
||||
} else if let result = result as? PHAsset {
|
||||
controller.updateHiddenMediaId(result.localIdentifier)
|
||||
if !forCollage {
|
||||
controller.updateHiddenMediaId(result.localIdentifier)
|
||||
}
|
||||
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
|
||||
let transitionOut: (Bool?) -> (UIView, CGRect)? = { isNew in
|
||||
if let isNew {
|
||||
|
|
@ -3107,7 +3115,7 @@ public func stickerMediaPickerController(
|
|||
controller.forceSourceRect = true
|
||||
controller.getSourceRect = getSourceRect
|
||||
controller.requestController = { [weak controller] _, present in
|
||||
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .createSticker), mainButtonState: nil, mainButtonAction: nil)
|
||||
let mediaPickerController = MediaPickerScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .createSticker), mainButtonState: nil, mainButtonAction: nil)
|
||||
mediaPickerController.customSelection = { controller, result in
|
||||
if let result = result as? PHAsset {
|
||||
controller.updateHiddenMediaId(result.localIdentifier)
|
||||
|
|
@ -3175,17 +3183,17 @@ public func stickerMediaPickerController(
|
|||
}
|
||||
|
||||
var returnToCameraImpl: (() -> Void)?
|
||||
let cameraScreen = CameraScreen(
|
||||
let cameraScreen = CameraScreenImpl(
|
||||
context: context,
|
||||
mode: .sticker,
|
||||
holder: cameraHolder,
|
||||
transitionIn: CameraScreen.TransitionIn(
|
||||
transitionIn: CameraScreenImpl.TransitionIn(
|
||||
sourceView: cameraHolder.parentView,
|
||||
sourceRect: cameraHolder.parentView.bounds,
|
||||
sourceCornerRadius: 0.0
|
||||
),
|
||||
transitionOut: { _ in
|
||||
return CameraScreen.TransitionOut(
|
||||
return CameraScreenImpl.TransitionOut(
|
||||
destinationView: cameraHolder.parentView,
|
||||
destinationRect: cameraHolder.parentView.bounds,
|
||||
destinationCornerRadius: 0.0
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import ChatMessageBackground
|
|||
import ChatSendMessageActionUI
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import MediaAssetsContext
|
||||
|
||||
private class MediaPickerSelectedItemNode: ASDisplayNode {
|
||||
let asset: TGMediaEditableItem
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ let package = Package(
|
|||
"MediaPlayerAudioRenderer.swift",
|
||||
"MediaPlayerFramePreview.swift",
|
||||
"VideoPlayerProxy.swift",
|
||||
"ChunkMediaPlayer.swift"
|
||||
"ChunkMediaPlayer.swift",
|
||||
"ChunkMediaPlayerV2.swift"
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import TelegramCore
|
|||
import TelegramAudio
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import VideoToolbox
|
||||
|
||||
public let isHardwareAv1Supported: Bool = {
|
||||
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
|
||||
return value
|
||||
}()
|
||||
|
||||
public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
|
||||
private final class LoadedPart {
|
||||
|
|
@ -33,10 +39,10 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
|
|||
|
||||
func load() {
|
||||
let reader: MediaDataReader
|
||||
if self.mediaType == .video && self.codecName == "av1" {
|
||||
if self.mediaType == .video && (self.codecName == "av1" || self.codecName == "av01") && isHardwareAv1Supported {
|
||||
reader = AVAssetVideoDataReader(filePath: self.tempFile.path, isVideo: self.mediaType == .video)
|
||||
} else {
|
||||
reader = FFMpegMediaDataReader(filePath: self.tempFile.path, isVideo: self.mediaType == .video)
|
||||
reader = FFMpegMediaDataReader(filePath: self.tempFile.path, isVideo: self.mediaType == .video, codecName: self.codecName)
|
||||
}
|
||||
if self.mediaType == .video {
|
||||
if reader.hasVideo {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
|
|||
private let codecContext: FFMpegAVCodecContext
|
||||
private let swrContext: FFMpegSWResample
|
||||
|
||||
private var timescale: CMTimeScale = 44000
|
||||
private let audioFrame: FFMpegAVFrame
|
||||
private var resetDecoderOnNextFrame = true
|
||||
|
||||
|
|
@ -59,31 +60,34 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? {
|
||||
func send(frame: MediaTrackDecodableFrame) -> Bool {
|
||||
self.timescale = frame.pts.timescale
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
while true {
|
||||
let result = self.codecContext.receive(into: self.audioFrame)
|
||||
if case .success = result {
|
||||
if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts) {
|
||||
self.delayedFrames.append(convertedFrame)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
return status == 0
|
||||
}
|
||||
|
||||
func decode() -> MediaTrackFrame? {
|
||||
while true {
|
||||
let result = self.codecContext.receive(into: self.audioFrame)
|
||||
if case .success = result {
|
||||
if let convertedFrame = convertAudioFrame(self.audioFrame) {
|
||||
self.delayedFrames.append(convertedFrame)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if self.delayedFrames.count >= 1 {
|
||||
var minFrameIndex = 0
|
||||
var minPosition = self.delayedFrames[0].position
|
||||
for i in 1 ..< self.delayedFrames.count {
|
||||
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
|
||||
minFrameIndex = i
|
||||
minPosition = self.delayedFrames[i].position
|
||||
}
|
||||
}
|
||||
|
||||
if self.delayedFrames.count >= 1 {
|
||||
var minFrameIndex = 0
|
||||
var minPosition = self.delayedFrames[0].position
|
||||
for i in 1 ..< self.delayedFrames.count {
|
||||
if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 {
|
||||
minFrameIndex = i
|
||||
minPosition = self.delayedFrames[i].position
|
||||
}
|
||||
}
|
||||
return self.delayedFrames.remove(at: minFrameIndex)
|
||||
}
|
||||
return self.delayedFrames.remove(at: minFrameIndex)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -121,7 +125,7 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
private func convertAudioFrame(_ frame: FFMpegAVFrame, pts: CMTime) -> MediaTrackFrame? {
|
||||
private func convertAudioFrame(_ frame: FFMpegAVFrame) -> MediaTrackFrame? {
|
||||
guard let data = self.swrContext.resample(frame) else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -137,6 +141,8 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder {
|
|||
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
|
||||
let pts = CMTime(value: frame.pts, timescale: self.timescale)
|
||||
|
||||
guard CMAudioSampleBufferCreateReadyWithPacketDescriptions(allocator: nil, dataBuffer: blockBuffer!, formatDescription: self.formatDescription, sampleCount: Int(data.count / 2), presentationTimeStamp: pts, packetDescriptions: nil, sampleBufferOut: &sampleBuffer) == noErr else {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,8 +283,15 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource {
|
|||
if let video = streamDescriptions.video {
|
||||
videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, startTime: video.startTime, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration)
|
||||
for videoFrame in streamDescriptions.extraVideoFrames {
|
||||
if let decodedFrame = video.decoder.decode(frame: videoFrame) {
|
||||
if !video.decoder.send(frame: videoFrame) {
|
||||
break
|
||||
}
|
||||
}
|
||||
while true {
|
||||
if let decodedFrame = video.decoder.decode() {
|
||||
extraDecodedVideoFrames.append(decodedFrame)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ struct FFMpegMediaFrameSourceDescriptionSet {
|
|||
}
|
||||
|
||||
private final class InitializedState {
|
||||
fileprivate let avIoContext: FFMpegAVIOContext
|
||||
fileprivate let avIoContext: FFMpegAVIOContext?
|
||||
fileprivate let avFormatContext: FFMpegAVFormatContext
|
||||
|
||||
fileprivate let audioStream: StreamContext?
|
||||
fileprivate let videoStream: StreamContext?
|
||||
|
||||
init(avIoContext: FFMpegAVIOContext, avFormatContext: FFMpegAVFormatContext, audioStream: StreamContext?, videoStream: StreamContext?) {
|
||||
init(avIoContext: FFMpegAVIOContext?, avFormatContext: FFMpegAVFormatContext, audioStream: StreamContext?, videoStream: StreamContext?) {
|
||||
self.avIoContext = avIoContext
|
||||
self.avFormatContext = avFormatContext
|
||||
self.audioStream = audioStream
|
||||
|
|
@ -297,7 +297,6 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
|||
fileprivate var streamable: Bool?
|
||||
fileprivate var statsCategory: MediaResourceStatsCategory?
|
||||
|
||||
private let ioBufferSize = 1 * 1024
|
||||
fileprivate var readingOffset: Int64 = 0
|
||||
|
||||
fileprivate var requestedDataOffset: Int64?
|
||||
|
|
@ -408,16 +407,43 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
let avFormatContext = FFMpegAVFormatContext()
|
||||
|
||||
guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(self.ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: isSeekable) else {
|
||||
self.readingError = true
|
||||
return
|
||||
var directFilePath: String?
|
||||
if !streamable {
|
||||
let data = postbox.mediaBox.resourceData(resourceReference.resource, pathExtension: nil, option: .complete(waitUntilFetchStatus: false))
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let _ = self.currentSemaphore.swap(semaphore)
|
||||
var resultFilePath: String?
|
||||
let disposable = data.start(next: { next in
|
||||
if next.complete {
|
||||
resultFilePath = next.path
|
||||
semaphore.signal()
|
||||
}
|
||||
})
|
||||
semaphore.wait()
|
||||
let _ = self.currentSemaphore.swap(nil)
|
||||
disposable.dispose()
|
||||
|
||||
if let resultFilePath {
|
||||
directFilePath = resultFilePath
|
||||
} else {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
avFormatContext.setIO(avIoContext)
|
||||
let avFormatContext = FFMpegAVFormatContext()
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
var avIoContext: FFMpegAVIOContext?
|
||||
if directFilePath == nil {
|
||||
guard let avIoContextValue = FFMpegAVIOContext(bufferSize: 64 * 1024, opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: isSeekable) else {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
avIoContext = avIoContextValue
|
||||
avFormatContext.setIO(avIoContextValue)
|
||||
}
|
||||
|
||||
if !avFormatContext.openInput(withDirectFilePath: directFilePath) {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
|
|
@ -643,7 +669,13 @@ final class FFMpegMediaFrameSourceContext: NSObject {
|
|||
for stream in [initializedState.videoStream, initializedState.audioStream] {
|
||||
if let stream = stream {
|
||||
let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale)
|
||||
#if DEBUG && false
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
#endif
|
||||
initializedState.avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: true)
|
||||
#if DEBUG && false
|
||||
print("Seek time: \(CFAbsoluteTimeGetCurrent() - startTime) s")
|
||||
#endif
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,24 @@ final class FFMpegMediaPassthroughVideoFrameDecoder: MediaTrackFrameDecoder {
|
|||
private let rotationAngle: Double
|
||||
private var resetDecoderOnNextFrame = true
|
||||
|
||||
private var sentFrameQueue: [MediaTrackDecodableFrame] = []
|
||||
|
||||
init(videoFormatData: VideoFormatData, rotationAngle: Double) {
|
||||
self.videoFormatData = videoFormatData
|
||||
self.rotationAngle = rotationAngle
|
||||
}
|
||||
|
||||
func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? {
|
||||
func send(frame: MediaTrackDecodableFrame) -> Bool {
|
||||
self.sentFrameQueue.append(frame)
|
||||
return true
|
||||
}
|
||||
|
||||
func decode() -> MediaTrackFrame? {
|
||||
guard let frame = self.sentFrameQueue.first else {
|
||||
return nil
|
||||
}
|
||||
self.sentFrameQueue.removeFirst()
|
||||
|
||||
if self.videoFormat == nil {
|
||||
if self.videoFormatData.codecType == kCMVideoCodecType_MPEG4Video {
|
||||
self.videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), self.videoFormatData.width, self.videoFormatData.height, self.videoFormatData.extraData)
|
||||
|
|
|
|||
|
|
@ -72,11 +72,10 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
|||
}
|
||||
|
||||
func decodeInternal(frame: MediaTrackDecodableFrame) {
|
||||
|
||||
}
|
||||
|
||||
public func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? {
|
||||
return self.decode(frame: frame, ptsOffset: nil)
|
||||
public func decode() -> MediaTrackFrame? {
|
||||
return self.decode(ptsOffset: nil)
|
||||
}
|
||||
|
||||
public func sendToDecoder(frame: MediaTrackDecodableFrame) -> Bool {
|
||||
|
|
@ -126,28 +125,36 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
public func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? {
|
||||
if self.isError {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func send(frame: MediaTrackDecodableFrame) -> Bool {
|
||||
let status = frame.packet.send(toDecoder: self.codecContext)
|
||||
if status == 0 {
|
||||
self.defaultDuration = frame.duration
|
||||
self.defaultTimescale = frame.pts.timescale
|
||||
|
||||
if self.codecContext.receive(into: self.videoFrame) == .success {
|
||||
if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 {
|
||||
self.isError = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: frame.pts.timescale)
|
||||
if let ptsOffset = ptsOffset {
|
||||
pts = CMTimeAdd(pts, ptsOffset)
|
||||
}
|
||||
return convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: frame.duration, forceARGB: forceARGB, unpremultiplyAlpha: unpremultiplyAlpha, displayImmediately: displayImmediately)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func decode(ptsOffset: CMTime?, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? {
|
||||
if self.isError {
|
||||
return nil
|
||||
}
|
||||
guard let defaultDuration = self.defaultDuration, let defaultTimescale = self.defaultTimescale else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.codecContext.receive(into: self.videoFrame) == .success {
|
||||
if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 {
|
||||
self.isError = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale)
|
||||
if let ptsOffset = ptsOffset {
|
||||
pts = CMTimeAdd(pts, ptsOffset)
|
||||
}
|
||||
return convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: defaultDuration, forceARGB: forceARGB, unpremultiplyAlpha: unpremultiplyAlpha, displayImmediately: displayImmediately)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -2,32 +2,44 @@ import Foundation
|
|||
import AVFoundation
|
||||
import CoreMedia
|
||||
import FFMpegBinding
|
||||
import VideoToolbox
|
||||
|
||||
protocol MediaDataReader: AnyObject {
|
||||
#if os(macOS)
|
||||
private let isHardwareAv1Supported: Bool = {
|
||||
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
|
||||
return value
|
||||
}()
|
||||
#endif
|
||||
|
||||
public protocol MediaDataReader: AnyObject {
|
||||
var hasVideo: Bool { get }
|
||||
var hasAudio: Bool { get }
|
||||
|
||||
func readSampleBuffer() -> CMSampleBuffer?
|
||||
}
|
||||
|
||||
final class FFMpegMediaDataReader: MediaDataReader {
|
||||
public final class FFMpegMediaDataReader: MediaDataReader {
|
||||
private let isVideo: Bool
|
||||
private let videoSource: SoftwareVideoReader?
|
||||
private let audioSource: SoftwareAudioSource?
|
||||
|
||||
var hasVideo: Bool {
|
||||
public var hasVideo: Bool {
|
||||
return self.videoSource != nil
|
||||
}
|
||||
|
||||
var hasAudio: Bool {
|
||||
public var hasAudio: Bool {
|
||||
return self.audioSource != nil
|
||||
}
|
||||
|
||||
init(filePath: String, isVideo: Bool) {
|
||||
public init(filePath: String, isVideo: Bool, codecName: String?) {
|
||||
self.isVideo = isVideo
|
||||
|
||||
if self.isVideo {
|
||||
let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: true)
|
||||
var passthroughDecoder = true
|
||||
if (codecName == "av1" || codecName == "av01") && !isHardwareAv1Supported {
|
||||
passthroughDecoder = false
|
||||
}
|
||||
let videoSource = SoftwareVideoReader(path: filePath, hintVP9: false, passthroughDecoder: passthroughDecoder)
|
||||
if videoSource.hasStream {
|
||||
self.videoSource = videoSource
|
||||
} else {
|
||||
|
|
@ -45,7 +57,7 @@ final class FFMpegMediaDataReader: MediaDataReader {
|
|||
}
|
||||
}
|
||||
|
||||
func readSampleBuffer() -> CMSampleBuffer? {
|
||||
public func readSampleBuffer() -> CMSampleBuffer? {
|
||||
if let videoSource {
|
||||
let frame = videoSource.readFrame()
|
||||
if let frame {
|
||||
|
|
@ -61,21 +73,21 @@ final class FFMpegMediaDataReader: MediaDataReader {
|
|||
}
|
||||
}
|
||||
|
||||
final class AVAssetVideoDataReader: MediaDataReader {
|
||||
public final class AVAssetVideoDataReader: MediaDataReader {
|
||||
private let isVideo: Bool
|
||||
private var mediaInfo: FFMpegMediaInfo.Info?
|
||||
private var assetReader: AVAssetReader?
|
||||
private var assetOutput: AVAssetReaderOutput?
|
||||
|
||||
var hasVideo: Bool {
|
||||
public var hasVideo: Bool {
|
||||
return self.assetOutput != nil
|
||||
}
|
||||
|
||||
var hasAudio: Bool {
|
||||
public var hasAudio: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
init(filePath: String, isVideo: Bool) {
|
||||
public init(filePath: String, isVideo: Bool) {
|
||||
self.isVideo = isVideo
|
||||
|
||||
if self.isVideo {
|
||||
|
|
@ -100,7 +112,7 @@ final class AVAssetVideoDataReader: MediaDataReader {
|
|||
}
|
||||
}
|
||||
|
||||
func readSampleBuffer() -> CMSampleBuffer? {
|
||||
public func readSampleBuffer() -> CMSampleBuffer? {
|
||||
guard let mediaInfo = self.mediaInfo, let assetReader = self.assetReader, let assetOutput = self.assetOutput else {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ public final class MediaPlayerNode: ASDisplayNode {
|
|||
private var videoNode: MediaPlayerNodeDisplayNode
|
||||
|
||||
public private(set) var videoLayer: AVSampleBufferDisplayLayer?
|
||||
private var videoLayerReadyForDisplayObserver: NSObjectProtocol?
|
||||
private var didNotifyVideoLayerReadyForDisplay: Bool = false
|
||||
|
||||
private let videoQueue: Queue
|
||||
|
||||
|
|
@ -217,7 +219,10 @@ public final class MediaPlayerNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
videoLayer.enqueue(frame.sampleBuffer)
|
||||
strongSelf.hasSentFramesToDisplay?()
|
||||
if #available(iOS 17.4, *) {
|
||||
} else {
|
||||
strongSelf.hasSentFramesToDisplay?()
|
||||
}
|
||||
}
|
||||
}
|
||||
Queue.mainQueue().async {
|
||||
|
|
@ -252,7 +257,7 @@ public final class MediaPlayerNode: ASDisplayNode {
|
|||
return
|
||||
}
|
||||
videoLayer.enqueue(frame.sampleBuffer)
|
||||
strongSelf.hasSentFramesToDisplay?()
|
||||
//strongSelf.hasSentFramesToDisplay?()
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
|
|
@ -350,6 +355,18 @@ public final class MediaPlayerNode: ASDisplayNode {
|
|||
|
||||
strongSelf.layer.addSublayer(videoLayer)
|
||||
|
||||
if #available(iOS 17.4, *) {
|
||||
strongSelf.videoLayerReadyForDisplayObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVSampleBufferDisplayLayerReadyForDisplayDidChange, object: videoLayer, queue: .main, using: { [weak strongSelf] _ in
|
||||
guard let strongSelf else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.didNotifyVideoLayerReadyForDisplay {
|
||||
strongSelf.didNotifyVideoLayerReadyForDisplay = true
|
||||
strongSelf.hasSentFramesToDisplay?()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
strongSelf.updateState()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,8 +185,12 @@ public final class MediaTrackFrameBuffer {
|
|||
|
||||
if !self.frames.isEmpty {
|
||||
let frame = self.frames.removeFirst()
|
||||
if let decodedFrame = self.decoder.decode(frame: frame) {
|
||||
return .frame(decodedFrame)
|
||||
if self.decoder.send(frame: frame) {
|
||||
if let decodedFrame = self.decoder.decode() {
|
||||
return .frame(decodedFrame)
|
||||
} else {
|
||||
return .skipFrame
|
||||
}
|
||||
} else {
|
||||
return .skipFrame
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
protocol MediaTrackFrameDecoder {
|
||||
func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame?
|
||||
func send(frame: MediaTrackDecodableFrame) -> Bool
|
||||
func decode() -> MediaTrackFrame?
|
||||
func takeQueuedFrame() -> MediaTrackFrame?
|
||||
func takeRemainingFrame() -> MediaTrackFrame?
|
||||
func reset()
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public final class SoftwareVideoSource {
|
|||
|
||||
avFormatContext.setIO(self.avIoContext!)
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
|
|
@ -277,10 +277,14 @@ public final class SoftwareVideoSource {
|
|||
if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 {
|
||||
ptsOffset = maxPts
|
||||
}
|
||||
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
|
||||
result = (decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset, forceARGB: self.hintVP9, unpremultiplyAlpha: self.unpremultiplyAlpha), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
if videoStream.decoder.send(frame: decodableFrame) {
|
||||
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
|
||||
result = (decoder.decode(ptsOffset: ptsOffset, forceARGB: self.hintVP9, unpremultiplyAlpha: self.unpremultiplyAlpha), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
} else {
|
||||
result = (videoStream.decoder.decode(), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
}
|
||||
} else {
|
||||
result = (videoStream.decoder.decode(frame: decodableFrame), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
}
|
||||
} else {
|
||||
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop)
|
||||
|
|
@ -392,7 +396,7 @@ public final class SoftwareAudioSource {
|
|||
|
||||
avFormatContext.setIO(self.avIoContext!)
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
|
|
@ -518,11 +522,17 @@ public final class SoftwareAudioSource {
|
|||
return nil
|
||||
}
|
||||
|
||||
let (decodableFrame, _) = self.readDecodableFrame()
|
||||
if let decodableFrame = decodableFrame {
|
||||
return audioStream.decoder.decode(frame: decodableFrame)?.sampleBuffer
|
||||
} else {
|
||||
return nil
|
||||
while true {
|
||||
let (decodableFrame, _) = self.readDecodableFrame()
|
||||
if let decodableFrame = decodableFrame {
|
||||
if audioStream.decoder.send(frame: decodableFrame) {
|
||||
if let result = audioStream.decoder.decode() {
|
||||
return result.sampleBuffer
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -593,7 +603,7 @@ final class SoftwareVideoReader {
|
|||
|
||||
avFormatContext.setIO(self.avIoContext!)
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
||||
self.readingError = true
|
||||
return
|
||||
}
|
||||
|
|
@ -730,10 +740,14 @@ final class SoftwareVideoReader {
|
|||
while !self.readingError && !self.hasReadToEnd {
|
||||
if let decodableFrame = self.readDecodableFrame() {
|
||||
var result: (MediaTrackFrame?, CGFloat, CGFloat)
|
||||
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
|
||||
result = (decoder.decode(frame: decodableFrame, ptsOffset: nil, forceARGB: false, unpremultiplyAlpha: false, displayImmediately: false), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
|
||||
if videoStream.decoder.send(frame: decodableFrame) {
|
||||
if let decoder = videoStream.decoder as? FFMpegMediaVideoFrameDecoder {
|
||||
result = (decoder.decode(ptsOffset: nil, forceARGB: false, unpremultiplyAlpha: false, displayImmediately: false), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
|
||||
} else {
|
||||
result = (videoStream.decoder.decode(), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
|
||||
}
|
||||
} else {
|
||||
result = (videoStream.decoder.decode(frame: decodableFrame), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
|
||||
result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect))
|
||||
}
|
||||
if let frame = result.0 {
|
||||
return frame
|
||||
|
|
@ -839,7 +853,7 @@ public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? {
|
|||
|
||||
avFormatContext.setIO(avIoContext)
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ private final class UniversalSoftwareVideoSourceImpl {
|
|||
|
||||
self.cancelRead = cancelInitialization
|
||||
|
||||
let ioBufferSize = 1 * 1024
|
||||
let ioBufferSize = 64 * 1024
|
||||
|
||||
let isSeekable: Bool
|
||||
switch source {
|
||||
|
|
@ -174,7 +174,7 @@ private final class UniversalSoftwareVideoSourceImpl {
|
|||
}
|
||||
avFormatContext.setIO(avIoContext)
|
||||
|
||||
if !avFormatContext.openInput() {
|
||||
if !avFormatContext.openInput(withDirectFilePath: nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1056,7 +1056,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
|
|||
}
|
||||
pushControllerImpl?(controller)
|
||||
}, openChannelExample: {
|
||||
resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "durov")
|
||||
resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "durov", referrer: nil)
|
||||
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
|
|
|
|||
|
|
@ -1556,7 +1556,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
|
|||
dismissTooltipsImpl?()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}, shareLink: { invite in
|
||||
guard let inviteLink = invite.link else {
|
||||
return
|
||||
|
|
@ -1564,7 +1564,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
|
|||
let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData)
|
||||
shareController.actionCompleted = {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
presentControllerImpl?(shareController, nil)
|
||||
}, linkContextAction: { node, gesture in
|
||||
|
|
@ -1587,7 +1587,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
|
|||
dismissTooltipsImpl?()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
})
|
||||
})))
|
||||
|
|
|
|||
BIN
submodules/PremiumUI/Resources/coin
Normal file
BIN
submodules/PremiumUI/Resources/coin
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -56,7 +56,7 @@ private final class HeaderComponent: Component {
|
|||
|
||||
let coinSize = self.coin.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PremiumCoinComponent(isIntro: true, isVisible: true, hasIdleAnimations: true)),
|
||||
component: AnyComponent(PremiumCoinComponent(mode: .business, isIntro: true, isVisible: true, hasIdleAnimations: true)),
|
||||
environment: {},
|
||||
containerSize: containerSize
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1928,7 +1928,7 @@ public class PremiumBoostLevelsScreen: ViewController {
|
|||
UIPasteboard.general.string = link
|
||||
|
||||
if let previousController = controller?.navigationController?.viewControllers.reversed().first(where: { $0 !== controller }) as? ViewController {
|
||||
previousController.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(text: self.presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
previousController.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(title: nil, text: self.presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
},
|
||||
dismiss: { [weak controller] in
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue