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:
Isaac 2024-11-30 01:48:56 +04:00
commit 8b6389e2a8
11086 changed files with 1301189 additions and 830765 deletions

3
.gitmodules vendored
View file

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

View file

@ -1746,6 +1746,7 @@ ios_extension(
":SwiftSignalKitFramework",
":PostboxFramework",
":TelegramCoreFramework",
":TelegramUIFramework",
],
)

View file

@ -22,6 +22,7 @@ swift_library(
"//submodules/GZip:GZip",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/Utils/RangeSet",
"//submodules/Media/ConvertOpusToAAC",
],
visibility = [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,6 +25,7 @@ swift_library(
"//submodules/GZip:GZip",
"//third-party/ZipArchive:ZipArchive",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TelegramVoip",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

@ -19,6 +19,9 @@ objc_library(
deps = [
"//submodules/ffmpeg",
],
sdk_frameworks = [
"CoreMedia",
],
visibility = [
"//visibility:public",
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -259,7 +259,7 @@ public func galleryItemForEntry(
}
if isHLS {
content = HLSVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos)
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))
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,6 +33,7 @@ swift_library(
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -51,6 +51,7 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramUI/Components/MediaAssetsContext",
],
visibility = [
"//visibility:public",

View file

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

View file

@ -7,6 +7,7 @@ import TelegramPresentationData
import ItemListUI
import MergeLists
import Photos
import MediaAssetsContext
private struct MediaGroupsGridAlbumEntry: Comparable, Identifiable {
let theme: PresentationTheme

View file

@ -6,6 +6,7 @@ import ContextUI
import AccountContext
import TelegramPresentationData
import Photos
import MediaAssetsContext
struct MediaGroupItem {
let collection: PHAssetCollection

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ import ChatMessageBackground
import ChatSendMessageActionUI
import ComponentFlow
import ComponentDisplayAdapters
import MediaAssetsContext
private class MediaPickerSelectedItemNode: ASDisplayNode {
let asset: TGMediaEditableItem

View file

@ -43,7 +43,8 @@ let package = Package(
"MediaPlayerAudioRenderer.swift",
"MediaPlayerFramePreview.swift",
"VideoPlayerProxy.swift",
"ChunkMediaPlayer.swift"
"ChunkMediaPlayer.swift",
"ChunkMediaPlayerV2.swift"
]),
]
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

View file

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

View file

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