mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Various improvements
This commit is contained in:
parent
62f156440f
commit
81e11d6d36
102 changed files with 3073 additions and 558 deletions
|
|
@ -69,6 +69,13 @@ class ShareRootController: UIViewController {
|
|||
}), getExtensionContext: { [weak self] in
|
||||
return self?.extensionContext
|
||||
})
|
||||
|
||||
self.impl?.openUrl = { [weak self] url in
|
||||
guard let self, let url = URL(string: url) else {
|
||||
return
|
||||
}
|
||||
let _ = self.openURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
self.impl?.loadView()
|
||||
|
|
@ -93,4 +100,20 @@ class ShareRootController: UIViewController {
|
|||
super.viewDidLayoutSubviews()
|
||||
self.impl?.viewDidLayoutSubviews(view: self.view, traitCollection: self.traitCollection)
|
||||
}
|
||||
|
||||
@objc func openURL(_ url: URL) -> Bool {
|
||||
var responder: UIResponder? = self
|
||||
while responder != nil {
|
||||
if let application = responder as? UIApplication {
|
||||
if #available(iOS 18.0, *) {
|
||||
application.open(url, options: [:], completionHandler: nil)
|
||||
return true
|
||||
} else {
|
||||
return application.perform(#selector(openURL(_:)), with: url) != nil
|
||||
}
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public enum AccessType {
|
|||
case denied
|
||||
case restricted
|
||||
case unreachable
|
||||
case limited
|
||||
}
|
||||
|
||||
public enum TelegramAppBuildType {
|
||||
|
|
@ -323,6 +324,7 @@ public enum ResolvedUrl {
|
|||
case collectible(gift: StarGift.UniqueGift?)
|
||||
case messageLink(link: TelegramResolvedMessageLink?)
|
||||
case stars
|
||||
case shareStory(Int64)
|
||||
}
|
||||
|
||||
public enum ResolveUrlResult {
|
||||
|
|
@ -803,6 +805,9 @@ public protocol MediaPickerScreen: ViewController {
|
|||
func dismissAnimated()
|
||||
}
|
||||
|
||||
public protocol ChatQrCodeScreen: ViewController {
|
||||
}
|
||||
|
||||
public protocol MediaEditorScreenResult {
|
||||
var target: Stories.PendingTarget { get }
|
||||
}
|
||||
|
|
@ -1155,7 +1160,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||
|
||||
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
|
||||
|
||||
func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController
|
||||
func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping ([MediaEditorScreenResult], @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController
|
||||
|
||||
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
|
||||
|
||||
|
|
@ -1228,6 +1233,8 @@ public protocol SharedAccountContext: AnyObject {
|
|||
@available(iOS 13.0, *)
|
||||
func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController
|
||||
|
||||
func makeForumSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) -> ViewController
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
||||
func openCreateGroupCallUI(context: AccountContext, peerIds: [EnginePeer.Id], parentController: ViewController)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,16 @@ public enum MediaManagerPlayerType {
|
|||
case file
|
||||
}
|
||||
|
||||
public struct AudioRecorderResumeData {
|
||||
public let compressedData: Data
|
||||
public let resumeData: Data
|
||||
|
||||
public init(compressedData: Data, resumeData: Data) {
|
||||
self.compressedData = compressedData
|
||||
self.resumeData = resumeData
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MediaManager: AnyObject {
|
||||
var audioSession: ManagedAudioSession { get }
|
||||
var galleryHiddenMediaManager: GalleryHiddenMediaManager { get }
|
||||
|
|
@ -157,7 +167,12 @@ public protocol MediaManager: AnyObject {
|
|||
func setOverlayVideoNode(_ node: OverlayMediaItemNode?)
|
||||
func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool
|
||||
|
||||
func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal<ManagedAudioRecorder?, NoError>
|
||||
func audioRecorder(
|
||||
resumeData: AudioRecorderResumeData?,
|
||||
beginWithTone: Bool,
|
||||
applicationBindings: TelegramApplicationBindings,
|
||||
beganWithTone: @escaping (Bool) -> Void
|
||||
) -> Signal<ManagedAudioRecorder?, NoError>
|
||||
}
|
||||
|
||||
public enum GalleryHiddenMediaId: Hashable {
|
||||
|
|
@ -215,13 +230,17 @@ public enum AudioRecordingState: Equatable {
|
|||
|
||||
public struct RecordedAudioData {
|
||||
public let compressedData: Data
|
||||
public let resumeData: Data?
|
||||
public let duration: Double
|
||||
public let waveform: Data?
|
||||
public let trimRange: Range<Double>?
|
||||
|
||||
public init(compressedData: Data, duration: Double, waveform: Data?) {
|
||||
public init(compressedData: Data, resumeData: Data?, duration: Double, waveform: Data?, trimRange: Range<Double>?) {
|
||||
self.compressedData = compressedData
|
||||
self.resumeData = resumeData
|
||||
self.duration = duration
|
||||
self.waveform = waveform
|
||||
self.trimRange = trimRange
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,4 +254,6 @@ public protocol ManagedAudioRecorder: AnyObject {
|
|||
func resume()
|
||||
func stop()
|
||||
func takenRecordedData() -> Signal<RecordedAudioData?, NoError>
|
||||
|
||||
func updateTrimRange(start: Double, end: Double, updatedEnd: Bool, apply: Bool)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||
private var productsDisposable: Disposable?
|
||||
private var inProgress = false
|
||||
|
||||
private var paymentDisposable = MetaDisposable()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
|
|
@ -87,11 +89,12 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||
}
|
||||
|
||||
deinit {
|
||||
self.paymentDisposable.dispose()
|
||||
self.productsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func proceed() {
|
||||
guard let component = self.component, let storeProduct = self.products.first(where: { $0.id == component.storeProduct }) else {
|
||||
guard let component = self.component, let storeProduct = self.products.first(where: { $0.id == component.storeProduct }), !self.inProgress else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -107,10 +110,12 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||
}
|
||||
let presentationData = component.presentationData
|
||||
if available {
|
||||
let _ = (component.inAppPurchaseManager.buyProduct(storeProduct, quantity: 1, purpose: purpose)
|
||||
self.paymentDisposable.set((component.inAppPurchaseManager.buyProduct(storeProduct, quantity: 1, purpose: purpose)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
let _ = status
|
||||
let _ = self
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.inProgress = false
|
||||
}, error: { [weak self] error in
|
||||
guard let self, let controller = self.environment?.controller() else {
|
||||
return
|
||||
|
|
@ -144,7 +149,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||
//let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
//controller.present(alertController, in: .window(.root))
|
||||
}
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
self.inProgress = false
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ private let anonymousSavedMessagesIcon = generateTintedImage(image: UIImage(bund
|
|||
private let anonymousSavedMessagesDarkIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: UIColor(white: 1.0, alpha: 0.4))
|
||||
private let myNotesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/MyNotesIcon"), color: .white)
|
||||
private let cameraIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/CameraIcon"), color: .white)
|
||||
private let storyIcon = generateTintedImage(image: UIImage(bundleImageName: "Share/Story"), color: .white)
|
||||
|
||||
public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
|
||||
return Font.with(size: size, design: .round, weight: .bold)
|
||||
|
|
@ -96,6 +97,8 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex:
|
|||
colors = AvatarNode.savedMessagesColors
|
||||
} else if case .repostIcon = icon {
|
||||
colors = AvatarNode.repostColors
|
||||
} else if case .storyIcon = icon {
|
||||
colors = AvatarNode.repostColors
|
||||
} else if case .repliesIcon = icon {
|
||||
colors = AvatarNode.savedMessagesColors
|
||||
} else if case let .anonymousSavedMessagesIcon(isColored) = icon {
|
||||
|
|
@ -200,6 +203,7 @@ public enum AvatarNodeIcon: Equatable {
|
|||
case phoneIcon
|
||||
case repostIcon
|
||||
case cameraIcon
|
||||
case storyIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeImageOverride: Equatable {
|
||||
|
|
@ -215,6 +219,7 @@ public enum AvatarNodeImageOverride: Equatable {
|
|||
case phoneIcon
|
||||
case repostIcon
|
||||
case cameraIcon
|
||||
case storyIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeColorOverride {
|
||||
|
|
@ -575,6 +580,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||
case .cameraIcon:
|
||||
representation = nil
|
||||
icon = .cameraIcon
|
||||
case .storyIcon:
|
||||
representation = nil
|
||||
icon = .storyIcon
|
||||
}
|
||||
} else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil {
|
||||
representation = peer?.smallProfileImage
|
||||
|
|
@ -755,6 +763,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||
case .cameraIcon:
|
||||
representation = nil
|
||||
icon = .cameraIcon
|
||||
case .storyIcon:
|
||||
representation = nil
|
||||
icon = .storyIcon
|
||||
}
|
||||
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
|
||||
representation = peer?.smallProfileImage
|
||||
|
|
@ -1007,6 +1018,15 @@ public final class AvatarNode: ASDisplayNode {
|
|||
if let cameraIcon = cameraIcon {
|
||||
context.draw(cameraIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - cameraIcon.size.width) / 2.0), y: floor((bounds.size.height - cameraIcon.size.height) / 2.0)), size: cameraIcon.size))
|
||||
}
|
||||
} else if case .storyIcon = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: -factor)
|
||||
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
||||
|
||||
if let storyIcon = storyIcon {
|
||||
context.draw(storyIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - storyIcon.size.width) / 2.0), y: floor((bounds.size.height - storyIcon.size.height) / 2.0)), size: storyIcon.size))
|
||||
}
|
||||
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage {
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public extension Camera {
|
|||
case iPhone15ProMax
|
||||
case unknown
|
||||
|
||||
init(model: DeviceModel) {
|
||||
public init(model: DeviceModel) {
|
||||
switch model {
|
||||
case .iPodTouch1, .iPodTouch2, .iPodTouch3, .iPodTouch4, .iPodTouch5, .iPodTouch6, .iPodTouch7:
|
||||
self = .singleCamera
|
||||
|
|
|
|||
|
|
@ -284,17 +284,23 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||
public let fileSize: Int32
|
||||
public let duration: Int32
|
||||
public let waveform: AudioWaveform
|
||||
public let trimRange: Range<Double>?
|
||||
public let resumeData: Data?
|
||||
|
||||
public init(
|
||||
resource: LocalFileMediaResource,
|
||||
fileSize: Int32,
|
||||
duration: Int32,
|
||||
waveform: AudioWaveform
|
||||
waveform: AudioWaveform,
|
||||
trimRange: Range<Double>?,
|
||||
resumeData: Data?
|
||||
) {
|
||||
self.resource = resource
|
||||
self.fileSize = fileSize
|
||||
self.duration = duration
|
||||
self.waveform = waveform
|
||||
self.trimRange = trimRange
|
||||
self.resumeData = resumeData
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -309,6 +315,14 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||
let waveformData = try container.decode(Data.self, forKey: "wd")
|
||||
let waveformPeak = try container.decode(Int32.self, forKey: "wp")
|
||||
self.waveform = AudioWaveform(samples: waveformData, peak: waveformPeak)
|
||||
|
||||
if let trimLowerBound = try container.decodeIfPresent(Double.self, forKey: "tl"), let trimUpperBound = try container.decodeIfPresent(Double.self, forKey: "tu") {
|
||||
self.trimRange = trimLowerBound ..< trimUpperBound
|
||||
} else {
|
||||
self.trimRange = nil
|
||||
}
|
||||
|
||||
self.resumeData = try container.decode(Data.self, forKey: "rd")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -319,6 +333,15 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||
try container.encode(self.duration, forKey: "d")
|
||||
try container.encode(self.waveform.samples, forKey: "wd")
|
||||
try container.encode(self.waveform.peak, forKey: "wp")
|
||||
|
||||
if let trimRange = self.trimRange {
|
||||
try container.encode(trimRange.lowerBound, forKey: "tl")
|
||||
try container.encode(trimRange.upperBound, forKey: "tu")
|
||||
}
|
||||
|
||||
if let resumeData = self.resumeData {
|
||||
try container.encode(resumeData, forKey: "rd")
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: Audio, rhs: Audio) -> Bool {
|
||||
|
|
@ -334,6 +357,12 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable {
|
|||
if lhs.waveform != rhs.waveform {
|
||||
return false
|
||||
}
|
||||
if lhs.trimRange != rhs.trimRange {
|
||||
return false
|
||||
}
|
||||
if lhs.resumeData != rhs.resumeData {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ private enum ContactListNodeEntrySection: Int {
|
|||
private enum ContactListNodeEntryId: Hashable {
|
||||
case search
|
||||
case sort
|
||||
case permission(action: Bool)
|
||||
case permission(index: Int)
|
||||
case option(index: Int)
|
||||
case peerId(peerId: Int64, section: ContactListNodeEntrySection)
|
||||
case deviceContact(DeviceContactStableId)
|
||||
|
|
@ -64,10 +64,11 @@ private final class ContactListNodeInteraction {
|
|||
fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void
|
||||
fileprivate let deselectAll: () -> Void
|
||||
fileprivate let toggleSelection: ([EnginePeer], Bool) -> Void
|
||||
fileprivate let openContactAccessPicker: () -> Void
|
||||
|
||||
let itemHighlighting = ContactItemHighlighting()
|
||||
|
||||
init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void) {
|
||||
init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void, openContactAccessPicker: @escaping () -> Void) {
|
||||
self.activateSearch = activateSearch
|
||||
self.authorize = authorize
|
||||
self.suppressWarning = suppressWarning
|
||||
|
|
@ -77,6 +78,7 @@ private final class ContactListNodeInteraction {
|
|||
self.openStories = openStories
|
||||
self.deselectAll = deselectAll
|
||||
self.toggleSelection = toggleSelection
|
||||
self.openContactAccessPicker = openContactAccessPicker
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +99,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
case sort(PresentationTheme, PresentationStrings, ContactsSortOrder)
|
||||
case permissionInfo(PresentationTheme, String, String, Bool)
|
||||
case permissionEnable(PresentationTheme, String)
|
||||
case permissionLimited(PresentationTheme, PresentationStrings)
|
||||
case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings)
|
||||
case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, Bool, StoryData?, Bool, String?)
|
||||
|
||||
|
|
@ -107,9 +110,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
case .sort:
|
||||
return .sort
|
||||
case .permissionInfo:
|
||||
return .permission(action: false)
|
||||
return .permission(index: 0)
|
||||
case .permissionEnable:
|
||||
return .permission(action: true)
|
||||
return .permission(index: 1)
|
||||
case .permissionLimited:
|
||||
return .permission(index: 2)
|
||||
case let .option(index, _, _, _, _):
|
||||
return .option(index: index)
|
||||
case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, _, storyData, _, _):
|
||||
|
|
@ -143,6 +148,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: {
|
||||
interaction.authorize()
|
||||
})
|
||||
case .permissionLimited:
|
||||
//TODO:localize
|
||||
return LimitedPermissionItem(presentationData: ItemListPresentationData(presentationData), text: "You have limited Telegram from accessing all of your contacts.", action: {
|
||||
interaction.openContactAccessPicker()
|
||||
})
|
||||
case let .option(_, option, header, _, _):
|
||||
let style: ContactListActionItem.Style
|
||||
let height: ContactListActionItem.Height
|
||||
|
|
@ -268,6 +278,12 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .permissionLimited(lhsTheme, lhsStrings):
|
||||
if case let .permissionLimited(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .option(lhsIndex, lhsOption, lhsHeader, lhsTheme, lhsStrings):
|
||||
if case let .option(rhsIndex, rhsOption, rhsHeader, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsOption == rhsOption, lhsHeader?.id == rhsHeader?.id, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
|
||||
return true
|
||||
|
|
@ -361,9 +377,16 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
default:
|
||||
return true
|
||||
}
|
||||
case let .option(lhsIndex, _, _, _, _):
|
||||
case .permissionLimited:
|
||||
switch rhs {
|
||||
case .search, .sort, .permissionInfo, .permissionEnable:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .option(lhsIndex, _, _, _, _):
|
||||
switch rhs {
|
||||
case .search, .sort, .permissionInfo, .permissionEnable, .permissionLimited:
|
||||
return false
|
||||
case let .option(rhsIndex, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
|
|
@ -372,7 +395,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
}
|
||||
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData, _, _):
|
||||
switch rhs {
|
||||
case .search, .sort, .permissionInfo, .permissionEnable, .option:
|
||||
case .search, .sort, .permissionInfo, .permissionEnable, .permissionLimited, .option:
|
||||
return false
|
||||
case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData, _, _):
|
||||
if (lhsStoryData == nil) != (rhsStoryData == nil) {
|
||||
|
|
@ -402,6 +425,8 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
|
|||
let title = strings.Contacts_PermissionsTitle
|
||||
let text = strings.Contacts_PermissionsText
|
||||
switch authorizationStatus {
|
||||
case .limited:
|
||||
entries.append(.permissionLimited(theme, strings))
|
||||
case .denied:
|
||||
entries.append(.permissionInfo(theme, title, text, suppressed))
|
||||
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))
|
||||
|
|
@ -1119,6 +1144,7 @@ public final class ContactListNode: ASDisplayNode {
|
|||
public var suppressPermissionWarning: (() -> Void)?
|
||||
private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?
|
||||
public var openStories: ((EnginePeer, ASDisplayNode) -> Void)?
|
||||
public var openContactAccessPicker: (() -> Void)?
|
||||
|
||||
private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil)
|
||||
private let disposable = MetaDisposable()
|
||||
|
|
@ -1272,6 +1298,8 @@ public final class ContactListNode: ASDisplayNode {
|
|||
return state
|
||||
})
|
||||
self.updatedSelection?(peers, value)
|
||||
}, openContactAccessPicker: { [weak self] in
|
||||
self?.openContactAccessPicker?()
|
||||
})
|
||||
|
||||
self.indexNode.indexSelected = { [weak self] section in
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import ContextUI
|
|||
import ChatListHeaderComponent
|
||||
import ChatListTitleView
|
||||
import ComponentFlow
|
||||
import SwiftUI
|
||||
import ContactsUI
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
let controller: ViewController
|
||||
|
|
@ -242,6 +244,10 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
}
|
||||
self.openStories?(peer, sourceNode)
|
||||
}
|
||||
|
||||
self.contactListNode.openContactAccessPicker = {
|
||||
presentContactAccessPicker(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -540,3 +546,44 @@ private final class ContactContextExtractedContentSource: ContextExtractedConten
|
|||
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentContactAccessPicker(context: AccountContext) {
|
||||
if #available(iOS 18.0, *), let rootViewController = context.sharedContext.mainWindow?.viewController?.view.window?.rootViewController {
|
||||
var dismissImpl: (() -> Void)?
|
||||
let pickerView = ContactAccessPickerHostingView(completionHandler: { [weak rootViewController] ids in
|
||||
DispatchQueue.main.async(execute: {
|
||||
guard let presentedController = rootViewController?.presentedViewController, presentedController.isBeingDismissed == false else { return }
|
||||
dismissImpl?()
|
||||
})
|
||||
})
|
||||
let hostingController = UIHostingController(rootView: pickerView)
|
||||
hostingController.view.isHidden = true
|
||||
hostingController.modalPresentationStyle = .overCurrentContext
|
||||
rootViewController.present(hostingController, animated: true)
|
||||
dismissImpl = { [weak hostingController] in
|
||||
Queue.mainQueue().after(0.4, {
|
||||
hostingController?.dismiss(animated: false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct ContactAccessPickerHostingView: View {
|
||||
@State var presented = true
|
||||
var handler: ([String]) -> ()
|
||||
|
||||
init(completionHandler: @escaping ([String]) -> ()) {
|
||||
self.handler = completionHandler
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Spacer()
|
||||
.contactAccessPicker(isPresented: $presented, completionHandler: handler)
|
||||
.onChange(of: presented) { newValue in
|
||||
if newValue == false {
|
||||
handler([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
256
submodules/ContactListUI/Sources/LimitedPermissionItem.swift
Normal file
256
submodules/ContactListUI/Sources/LimitedPermissionItem.swift
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TextFormat
|
||||
import Markdown
|
||||
import ItemListUI
|
||||
|
||||
public class LimitedPermissionItem: ListViewItem {
|
||||
public let selectable: Bool = false
|
||||
|
||||
let presentationData: ItemListPresentationData
|
||||
let text: String
|
||||
let action: (() -> Void)?
|
||||
|
||||
public init(
|
||||
presentationData: ItemListPresentationData,
|
||||
text: String,
|
||||
action: (() -> Void)?
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.text = text
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = LimitedPermissionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, nil)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? LimitedPermissionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, nil)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LimitedPermissionItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
|
||||
private let actionButton: HighlightableButtonNode
|
||||
private let actionButtonTitleNode: TextNode
|
||||
private let actionButtonBackgroundNode: ASImageNode
|
||||
|
||||
private let textNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: LimitedPermissionItem?
|
||||
|
||||
public override var canBeSelected: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
self.activateArea.accessibilityTraits = .staticText
|
||||
|
||||
self.actionButton = HighlightableButtonNode()
|
||||
|
||||
self.actionButtonBackgroundNode = ASImageNode()
|
||||
self.actionButtonBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.actionButtonTitleNode = TextNode()
|
||||
self.actionButtonTitleNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
self.addSubnode(self.actionButtonBackgroundNode)
|
||||
self.addSubnode(self.actionButtonTitleNode)
|
||||
self.addSubnode(self.actionButton)
|
||||
|
||||
self.actionButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.actionButtonBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.actionButtonBackgroundNode.alpha = 0.4
|
||||
strongSelf.actionButtonTitleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.actionButtonTitleNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.actionButtonBackgroundNode.alpha = 1.0
|
||||
strongSelf.actionButtonBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.actionButtonTitleNode.alpha = 1.0
|
||||
strongSelf.actionButtonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.actionButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: LimitedPermissionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
let makeButtonTitleLayout = TextNode.asyncLayout(self.actionButtonTitleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset
|
||||
let rightInset: CGFloat = 16.0 + params.rightInset
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let insets: UIEdgeInsets
|
||||
if let neighbors = neighbors {
|
||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
} else {
|
||||
insets = UIEdgeInsets()
|
||||
}
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
let itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
let attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
||||
//TODO:localize
|
||||
let (buttonTextLayout, buttonTextApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "MANAGE", font: Font.semibold(15.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - buttonTextLayout.size.width - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: textLayout.size.height + 20.0)
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.accessibilityLabel = attributedText.string
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = strongSelf.accessibilityLabel
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
|
||||
strongSelf.actionButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 14.0 * 2.0, color: item.presentationData.theme.list.itemCheckColors.fillColor, strokeColor: nil, strokeWidth: nil, backgroundColor: nil)
|
||||
}
|
||||
|
||||
let _ = textApply()
|
||||
let _ = buttonTextApply()
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
|
||||
if let neighbors = neighbors {
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
}
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
if let neighbors = neighbors {
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
strongSelf.bottomStripeNode.isHidden = true
|
||||
}
|
||||
} else {
|
||||
bottomStripeInset = leftInset
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 10.0), size: textLayout.size)
|
||||
|
||||
let actionButtonSize = CGSize(width: max(buttonTextLayout.size.width + 26.0, 40.0), height: 28.0)
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - actionButtonSize.width - 10.0, y: floor((layout.size.height - actionButtonSize.height) / 2.0)), size: actionButtonSize)
|
||||
strongSelf.actionButton.frame = actionButtonFrame
|
||||
strongSelf.actionButtonBackgroundNode.frame = actionButtonFrame
|
||||
strongSelf.actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: actionButtonFrame.minX + floorToScreenPixels((actionButtonFrame.width - buttonTextLayout.size.width) / 2.0), y: actionButtonFrame.minY + floorToScreenPixels((actionButtonFrame.height - buttonTextLayout.size.height) / 2.0) + 1.0 - UIScreenPixel), size: buttonTextLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
public override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
public override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
if let item = self.item {
|
||||
item.action?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,6 +161,8 @@ public final class DeviceAccess {
|
|||
subscriber.putNext(.notDetermined)
|
||||
case .authorized:
|
||||
subscriber.putNext(.allowed)
|
||||
case .limited:
|
||||
subscriber.putNext(.limited)
|
||||
default:
|
||||
subscriber.putNext(.denied)
|
||||
}
|
||||
|
|
@ -181,7 +183,7 @@ public final class DeviceAccess {
|
|||
return status
|
||||
|> then(self.contacts
|
||||
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
|
||||
if let authorized = authorized {
|
||||
if let authorized {
|
||||
return .single(authorized ? .allowed : .denied)
|
||||
} else {
|
||||
return .complete()
|
||||
|
|
@ -540,6 +542,9 @@ public final class DeviceAccess {
|
|||
case .authorized:
|
||||
self.contactsPromise.set(.single(true))
|
||||
completion(true)
|
||||
case .limited:
|
||||
self.contactsPromise.set(.single(true))
|
||||
completion(true)
|
||||
default:
|
||||
self.contactsPromise.set(.single(false))
|
||||
completion(false)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FFMpegOpusTrimmer : NSObject
|
||||
|
||||
+ (bool)trim:(NSString * _Nonnull)path
|
||||
to:(NSString * _Nonnull)outputPath
|
||||
start:(double)start
|
||||
end:(double)end;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
108
submodules/FFMpegBinding/Sources/FFMpegOpusTrimmer.m
Normal file
108
submodules/FFMpegBinding/Sources/FFMpegOpusTrimmer.m
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#import <FFMpegBinding/FFMpegOpusTrimmer.h>
|
||||
|
||||
#import "libavformat/avformat.h"
|
||||
#import "libavutil/avutil.h"
|
||||
|
||||
@implementation FFMpegOpusTrimmer
|
||||
|
||||
+ (bool)trim:(NSString * _Nonnull)inputPath
|
||||
to:(NSString * _Nonnull)outputPath
|
||||
start:(double)start
|
||||
end:(double)end
|
||||
{
|
||||
AVFormatContext *inCtx = NULL;
|
||||
int ret;
|
||||
if ((ret = avformat_open_input(&inCtx, inputPath.UTF8String, NULL, NULL)) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ret = avformat_find_stream_info(inCtx, NULL)) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int audioIdx = -1;
|
||||
for (unsigned i = 0; i < inCtx->nb_streams; ++i) {
|
||||
if (inCtx->streams[i]->codecpar->codec_id == AV_CODEC_ID_OPUS) {
|
||||
audioIdx = (int)i; break;
|
||||
}
|
||||
}
|
||||
if (audioIdx == -1) {
|
||||
avformat_close_input(&inCtx);
|
||||
return false;
|
||||
}
|
||||
AVStream *inSt = inCtx->streams[audioIdx];
|
||||
AVRational tb = inSt->time_base;
|
||||
|
||||
AVFormatContext *outCtx = NULL;
|
||||
avformat_alloc_output_context2(&outCtx, NULL, "ogg",
|
||||
outputPath.UTF8String);
|
||||
if (!outCtx) {
|
||||
avformat_close_input(&inCtx);
|
||||
return false;
|
||||
}
|
||||
|
||||
AVStream *outSt = avformat_new_stream(outCtx, NULL);
|
||||
avcodec_parameters_copy(outSt->codecpar, inSt->codecpar);
|
||||
outSt->time_base = tb;
|
||||
|
||||
if (!(outCtx->oformat->flags & AVFMT_NOFILE)) {
|
||||
if (avio_open(&outCtx->pb, outputPath.UTF8String, AVIO_FLAG_WRITE) < 0) {
|
||||
avformat_free_context(outCtx);
|
||||
avformat_close_input(&inCtx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ret = avformat_write_header(outCtx, NULL);
|
||||
|
||||
int64_t startTs = (int64_t)(start / av_q2d(tb));
|
||||
int64_t endTs = (int64_t)(end / av_q2d(tb));
|
||||
//int64_t span = MAX(endTs - startTs, 1);
|
||||
av_seek_frame(inCtx, audioIdx, startTs, AVSEEK_FLAG_BACKWARD);
|
||||
|
||||
AVPacket *pkt = nil;
|
||||
pkt = av_packet_alloc();
|
||||
//double lastPct = 0.0;
|
||||
|
||||
int64_t firstPts = startTs;
|
||||
|
||||
while (av_read_frame(inCtx, pkt) >= 0) {
|
||||
if (pkt->stream_index != audioIdx) { av_packet_unref(pkt); continue; }
|
||||
if (pkt->pts < startTs) { av_packet_unref(pkt); continue; }
|
||||
if (pkt->pts > endTs) { av_packet_unref(pkt); break; }
|
||||
|
||||
//double pct = (double)(pkt.pts - startTs) / (double)span;
|
||||
//if (pct - lastPct >= 0.01 && progress) {
|
||||
// lastPct = pct;
|
||||
//dispatch_async(dispatch_get_main_queue(), ^{ progress(pct); });
|
||||
//}
|
||||
|
||||
pkt->pts = av_rescale_q(pkt->pts - firstPts, tb, outSt->time_base);
|
||||
pkt->dts = av_rescale_q(pkt->dts - firstPts, tb, outSt->time_base);
|
||||
pkt->duration = av_rescale_q(pkt->duration, tb, outSt->time_base);
|
||||
pkt->pos = -1;
|
||||
pkt->stream_index = 0;
|
||||
|
||||
if (av_interleaved_write_frame(outCtx, pkt) != 0) {
|
||||
av_packet_unref(pkt);
|
||||
av_write_trailer(outCtx);
|
||||
avio_closep(&outCtx->pb);
|
||||
avformat_free_context(outCtx);
|
||||
avformat_close_input(&inCtx);
|
||||
return false;
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
}
|
||||
|
||||
av_write_trailer(outCtx);
|
||||
avio_closep(&outCtx->pb);
|
||||
avformat_free_context(outCtx);
|
||||
avformat_close_input(&inCtx);
|
||||
|
||||
return true;
|
||||
// if (progress) {
|
||||
// dispatch_async(dispatch_get_main_queue(), ^{ progress(1.0); });
|
||||
// }
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -64,6 +64,8 @@ typedef enum {
|
|||
- (TGMediaAssetsPallete *)mediaAssetsPallete;
|
||||
- (TGCheckButtonPallete *)checkButtonPallete;
|
||||
|
||||
- (NSArray<NSNumber *> *)cameraZoomLevels;
|
||||
|
||||
@required
|
||||
|
||||
- (CGFloat)applicationStatusBarAlpha;
|
||||
|
|
@ -89,7 +91,9 @@ typedef enum {
|
|||
- (NSDictionary *)serverMediaDataForAssetUrl:(NSString *)url;
|
||||
|
||||
- (void)presentActionSheet:(NSArray<LegacyComponentsActionSheetAction *> *)actions view:(UIView *)view completion:(void (^)(LegacyComponentsActionSheetAction *))completion;
|
||||
- (void)presentActionSheet:(NSArray<LegacyComponentsActionSheetAction *> *)actions view:(UIView *)view sourceRect:(CGRect (^)(void))sourceRect completion:(void (^)(LegacyComponentsActionSheetAction *))completion ;
|
||||
- (void)presentActionSheet:(NSArray<LegacyComponentsActionSheetAction *> *)actions view:(UIView *)view sourceRect:(CGRect (^)(void))sourceRect completion:(void (^)(LegacyComponentsActionSheetAction *))completion;
|
||||
|
||||
- (void)presentTooltip:(NSString *)text icon:(UIImage *)icon sourceRect:(CGRect)sourceRect;
|
||||
|
||||
- (id<LegacyComponentsOverlayWindowManager>)makeOverlayWindowManager;
|
||||
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ typedef enum
|
|||
@property (nonatomic, assign) CGFloat zoomLevel;
|
||||
@property (nonatomic, readonly) CGFloat minZoomLevel;
|
||||
@property (nonatomic, readonly) CGFloat maxZoomLevel;
|
||||
@property (nonatomic, readonly) int32_t maxMarkZoomValue;
|
||||
@property (nonatomic, readonly) int32_t secondMarkZoomValue;
|
||||
|
||||
- (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated;
|
||||
|
||||
|
|
@ -101,6 +99,8 @@ typedef enum
|
|||
|
||||
@property (nonatomic, assign) bool autoStartVideoRecording;
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSNumber *> *zoomLevels;
|
||||
|
||||
- (instancetype)initWithMode:(PGCameraMode)mode position:(PGCameraPosition)position;
|
||||
|
||||
- (void)attachPreviewView:(TGCameraPreviewView *)previewView;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@
|
|||
@property (nonatomic, readonly) CGFloat minZoomLevel;
|
||||
@property (nonatomic, readonly) CGFloat maxZoomLevel;
|
||||
|
||||
@property (nonatomic, readonly) int32_t maxMarkZoomValue;
|
||||
@property (nonatomic, readonly) int32_t secondMarkZoomValue;
|
||||
@property (nonatomic, strong) NSArray<NSNumber *> *zoomLevels;
|
||||
|
||||
- (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated;
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
@property (nonatomic, assign) CGRect previewViewFrame;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera;
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault camera:(PGCamera *)camera;
|
||||
|
||||
- (void)setDocumentFrameHidden:(bool)hidden;
|
||||
- (void)setCameraMode:(PGCameraMode)mode;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
- (void)panGesture:(UIPanGestureRecognizer *)gestureRecognizer;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel;
|
||||
- (instancetype)initWithFrame:(CGRect)frame zoomLevels:(NSArray<NSNumber *> *)zoomLevels minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
|||
|
|
@ -110,11 +110,13 @@
|
|||
- (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;
|
||||
|
||||
- (bool)isHighQualityPhoto;
|
||||
- (SSignal *)highQualityPhoto;
|
||||
- (void)setHighQualityPhoto:(bool)highQualityPhoto;
|
||||
|
||||
- (SSignal *)facesForItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
- (void)setFaces:(NSArray *)faces forItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
- (void)setAllInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused delay animated:(bool)animated;
|
||||
- (void)setToolbarsHidden:(bool)hidden animated:(bool)animated;
|
||||
|
||||
- (void)showPhotoQualityTooltip:(bool)hd;
|
||||
|
||||
- (void)immediateEditorTransitionIn;
|
||||
- (void)editorTransitionIn;
|
||||
- (void)editorTransitionOut;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
+ (UIImage *)gifActiveIcon;
|
||||
+ (UIImage *)muteIcon;
|
||||
+ (UIImage *)muteActiveIcon;
|
||||
+ (UIImage *)qualityIconForHighQuality:(bool)highQuality filled:(bool)filled;
|
||||
+ (UIImage *)qualityIconForPreset:(TGMediaVideoConversionPreset)preset;
|
||||
+ (UIImage *)timerIconForValue:(NSInteger)value;
|
||||
+ (UIImage *)eraserIcon;
|
||||
|
|
|
|||
|
|
@ -760,19 +760,12 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
|||
{
|
||||
if (self.disabled)
|
||||
return;
|
||||
|
||||
|
||||
self.captureSession.zoomLevels = self.zoomLevels;
|
||||
[self.captureSession setZoomLevel:zoomLevel animated:animated];
|
||||
}];
|
||||
}
|
||||
|
||||
- (int32_t)maxMarkZoomValue {
|
||||
return self.captureSession.maxMarkZoomValue;
|
||||
}
|
||||
|
||||
- (int32_t)secondMarkZoomValue {
|
||||
return self.captureSession.secondMarkZoomValue;
|
||||
}
|
||||
|
||||
#pragma mark - Device Angle
|
||||
|
||||
- (void)startDeviceAngleMeasuring
|
||||
|
|
|
|||
|
|
@ -504,9 +504,9 @@ const NSInteger PGCameraFrameRate = 30;
|
|||
if (backingLevel < firstMark) {
|
||||
realLevel = 0.5 + 0.5 * (backingLevel - 1.0) / (firstMark - 1.0);
|
||||
} else if (backingLevel < secondMark) {
|
||||
realLevel = 1.0 + 1.0 * (backingLevel - firstMark) / (secondMark - firstMark);
|
||||
realLevel = 1.0 + ([self secondMarkZoomValue] - 1.0) * (backingLevel - firstMark) / (secondMark - firstMark);
|
||||
} else {
|
||||
realLevel = 2.0 + 6.0 * (backingLevel - secondMark) / (self.maxZoomLevel - secondMark);
|
||||
realLevel = [self secondMarkZoomValue] + 6.0 * (backingLevel - secondMark) / (self.maxZoomLevel - secondMark);
|
||||
}
|
||||
} else if (marks.count == 1) {
|
||||
CGFloat mark = [marks.firstObject floatValue];
|
||||
|
|
@ -551,12 +551,12 @@ const NSInteger PGCameraFrameRate = 30;
|
|||
[self setZoomLevel:zoomLevel animated:false];
|
||||
}
|
||||
|
||||
- (int32_t)maxMarkZoomValue {
|
||||
return 25.0;
|
||||
}
|
||||
|
||||
- (int32_t)secondMarkZoomValue {
|
||||
return 5.0;
|
||||
- (CGFloat)secondMarkZoomValue {
|
||||
if (self.zoomLevels.count > 2) {
|
||||
return self.zoomLevels.lastObject.doubleValue;
|
||||
} else {
|
||||
return 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated
|
||||
|
|
@ -582,10 +582,10 @@ const NSInteger PGCameraFrameRate = 30;
|
|||
if (level < 1.0) {
|
||||
level = MAX(0.5, level);
|
||||
backingLevel = 1.0 + ((level - 0.5) / 0.5) * (firstMark - 1.0);
|
||||
} else if (zoomLevel < 2.0) {
|
||||
backingLevel = firstMark + ((level - 1.0) / 1.0) * (secondMark - firstMark);
|
||||
} else if (zoomLevel < [self secondMarkZoomValue]) {
|
||||
backingLevel = firstMark + ((level - 1.0) / ([self secondMarkZoomValue] - 1.0)) * (secondMark - firstMark);
|
||||
} else {
|
||||
backingLevel = secondMark + ((level - 2.0) / 6.0) * (self.maxZoomLevel - secondMark);
|
||||
backingLevel = secondMark + ((level - [self secondMarkZoomValue]) / 6.0) * (self.maxZoomLevel - secondMark);
|
||||
}
|
||||
} else if (marks.count == 1) {
|
||||
CGFloat mark = [marks.firstObject floatValue];
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
if (iosMajorVersion() >= 10) {
|
||||
_feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
||||
}
|
||||
|
||||
_camera.zoomLevels = context.cameraZoomLevels;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
@ -307,12 +309,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
|
||||
{
|
||||
_interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera camera:_camera];
|
||||
_interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent camera:_camera];
|
||||
[_interfaceView setInterfaceOrientation:interfaceOrientation animated:false];
|
||||
}
|
||||
else
|
||||
{
|
||||
_interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera camera:_camera];
|
||||
_interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent camera:_camera];
|
||||
[_interfaceView setInterfaceOrientation:interfaceOrientation animated:false];
|
||||
|
||||
CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation];
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
@synthesize cancelPressed;
|
||||
@synthesize actionHandle = _actionHandle;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault camera:(PGCamera *)camera
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self != nil)
|
||||
|
|
@ -245,7 +245,13 @@
|
|||
_topPanelBackgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor];
|
||||
[_topPanelView addSubview:_topPanelBackgroundView];
|
||||
|
||||
_zoomModeView = [[TGCameraZoomModeView alloc] initWithFrame:CGRectMake(floor((frame.size.width - 129.0) / 2.0), frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18 - 43, 129, 43) hasUltrawideCamera:hasUltrawideCamera hasTelephotoCamera:hasTelephotoCamera minZoomLevel:hasUltrawideCamera ? 0.5 : 1.0 maxZoomLevel:8.0];
|
||||
bool hasMultipleCameras = camera.zoomLevels.count > 1;
|
||||
CGFloat minZoomLevel = 1.0f;
|
||||
if (camera.zoomLevels.firstObject != nil) {
|
||||
minZoomLevel = camera.zoomLevels.firstObject.doubleValue;
|
||||
}
|
||||
|
||||
_zoomModeView = [[TGCameraZoomModeView alloc] initWithFrame:CGRectMake(floor((frame.size.width - 172.0) / 2.0), frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18 - 43, 172, 43) zoomLevels: camera.zoomLevels minZoomLevel:minZoomLevel maxZoomLevel:8.0];
|
||||
_zoomModeView.zoomChanged = ^(CGFloat zoomLevel, bool done, bool animated) {
|
||||
__strong TGCameraMainPhoneView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
|
|
@ -278,28 +284,15 @@
|
|||
strongSelf.zoomChanged(zoomLevel, animated);
|
||||
};
|
||||
[_zoomModeView setZoomLevel:1.0];
|
||||
if (hasTelephotoCamera || hasUltrawideCamera) {
|
||||
if (hasMultipleCameras) {
|
||||
[self addSubview:_zoomModeView];
|
||||
}
|
||||
|
||||
_zoomWheelView = [[TGCameraZoomWheelView alloc] initWithFrame:CGRectMake(0.0, frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 132, frame.size.width, 132) hasUltrawideCamera:hasUltrawideCamera hasTelephotoCamera:hasTelephotoCamera];
|
||||
[_zoomWheelView setHidden:true animated:false];
|
||||
[_zoomWheelView setZoomLevel:1.0];
|
||||
_zoomWheelView.panGesture = ^(UIPanGestureRecognizer *gestureRecognizer) {
|
||||
__strong TGCameraMainPhoneView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
[strongSelf->_zoomModeView panGesture:gestureRecognizer];
|
||||
};
|
||||
if (hasTelephotoCamera || hasUltrawideCamera) {
|
||||
[self addSubview:_zoomWheelView];
|
||||
}
|
||||
|
||||
|
||||
_zoomView = [[TGCameraZoomView alloc] initWithFrame:CGRectMake(10, frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18, frame.size.width - 20, 1.5f)];
|
||||
_zoomView.activityChanged = ^(bool active)
|
||||
{
|
||||
};
|
||||
if (!hasTelephotoCamera && !hasUltrawideCamera) {
|
||||
if (!hasMultipleCameras) {
|
||||
[self addSubview:_zoomView];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f;
|
|||
@synthesize shutterReleased;
|
||||
@synthesize cancelPressed;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault camera:(PGCamera *)camera
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self != nil)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
@dynamic thumbnailSignalForItem;
|
||||
@dynamic editingContext;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera {
|
||||
- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar camera:(PGCamera *)camera {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,54 +231,44 @@
|
|||
|
||||
@interface TGCameraZoomModeView () <UIGestureRecognizerDelegate>
|
||||
{
|
||||
NSArray<NSNumber *> *_zoomLevels;
|
||||
CGFloat _minZoomLevel;
|
||||
CGFloat _maxZoomLevel;
|
||||
|
||||
UIView *_backgroundView;
|
||||
|
||||
bool _hasUltrawideCamera;
|
||||
bool _hasTelephotoCamera;
|
||||
|
||||
NSMutableArray<TGCameraZoomModeItemView *> *_zoomItems;
|
||||
|
||||
bool _beganFromPress;
|
||||
|
||||
TGCameraZoomModeItemView *_leftItem;
|
||||
TGCameraZoomModeItemView *_centerItem;
|
||||
TGCameraZoomModeItemView *_rightItem;
|
||||
|
||||
bool _lockedOn;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation TGCameraZoomModeView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel
|
||||
- (instancetype)initWithFrame:(CGRect)frame zoomLevels:(NSArray<NSNumber *> *)zoomLevels minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self != nil)
|
||||
{
|
||||
_hasUltrawideCamera = hasUltrawideCamera;
|
||||
_hasTelephotoCamera = hasTelephotoCamera;
|
||||
_zoomLevels = [zoomLevels copy];
|
||||
_minZoomLevel = minZoomLevel;
|
||||
_maxZoomLevel = maxZoomLevel;
|
||||
|
||||
_backgroundView = [[UIView alloc] initWithFrame:self.bounds];
|
||||
_backgroundView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.15];
|
||||
_backgroundView.layer.cornerRadius = self.bounds.size.height / 2.0;
|
||||
|
||||
_leftItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(0, 0, 43, 43)];
|
||||
[_leftItem addTarget:self action:@selector(leftPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
_centerItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(43, 0, 43, 43)];
|
||||
[_centerItem addTarget:self action:@selector(centerPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
_rightItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(86, 0, 43, 43)];
|
||||
[_rightItem addTarget:self action:@selector(rightPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
[self addSubview:_backgroundView];
|
||||
[self addSubview:_centerItem];
|
||||
if (hasTelephotoCamera && hasUltrawideCamera) {
|
||||
[self addSubview:_leftItem];
|
||||
[self addSubview:_rightItem];
|
||||
|
||||
_zoomItems = [NSMutableArray array];
|
||||
|
||||
CGFloat itemWidth = 43.0;
|
||||
|
||||
for (NSInteger i = 0; i < zoomLevels.count; i++) {
|
||||
TGCameraZoomModeItemView *item = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(i * itemWidth, 0, itemWidth, itemWidth)];
|
||||
[item addTarget:self action:@selector(itemPressed:) forControlEvents:UIControlEventTouchUpInside];
|
||||
item.tag = i;
|
||||
[_zoomItems addObject:item];
|
||||
[self addSubview:item];
|
||||
}
|
||||
|
||||
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
|
||||
|
|
@ -288,6 +278,13 @@
|
|||
UILongPressGestureRecognizer *pressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(pressGesture:)];
|
||||
pressGestureRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:pressGestureRecognizer];
|
||||
|
||||
NSUInteger defaultIndex = [_zoomLevels indexOfObject:@(1.0)];
|
||||
if (defaultIndex == NSNotFound) {
|
||||
defaultIndex = 0;
|
||||
}
|
||||
_zoomLevel = [_zoomLevels[defaultIndex] floatValue];
|
||||
[self setZoomLevel:_zoomLevel animated:NO];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
@ -346,12 +343,14 @@
|
|||
}
|
||||
CGFloat newLevel = MAX(_minZoomLevel, MIN(_maxZoomLevel, _zoomLevel + delta));
|
||||
|
||||
CGFloat near = floor(newLevel);
|
||||
if (near <= 2.0 && ABS(newLevel - near) < 0.05 && previousLevel != near && translation.x < 15.0) {
|
||||
newLevel = near;
|
||||
_lockedOn = true;
|
||||
|
||||
[gestureRecognizer setTranslation:CGPointZero inView:self];
|
||||
for (NSNumber *level in _zoomLevels) {
|
||||
CGFloat zoomValue = [level floatValue];
|
||||
if (ABS(newLevel - zoomValue) < 0.05 && previousLevel != zoomValue && translation.x < 15.0) {
|
||||
newLevel = zoomValue;
|
||||
_lockedOn = true;
|
||||
[gestureRecognizer setTranslation:CGPointZero inView:self];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_zoomLevel = newLevel;
|
||||
|
|
@ -376,39 +375,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)leftPressed {
|
||||
if (_zoomLevel != 0.5) {
|
||||
[self setZoomLevel:0.5 animated:true];
|
||||
self.zoomChanged(0.5, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)centerPressed {
|
||||
if (!(_hasTelephotoCamera && _hasUltrawideCamera)) {
|
||||
if (_zoomLevel == 1.0) {
|
||||
if (_hasUltrawideCamera) {
|
||||
[self setZoomLevel:0.5 animated:true];
|
||||
self.zoomChanged(0.5, true, true);
|
||||
} else if (_hasTelephotoCamera) {
|
||||
[self setZoomLevel:2.0 animated:true];
|
||||
self.zoomChanged(2.0, true, true);
|
||||
}
|
||||
} else {
|
||||
[self setZoomLevel:1.0 animated:true];
|
||||
self.zoomChanged(1.0, true, true);
|
||||
- (void)itemPressed:(TGCameraZoomModeItemView *)sender {
|
||||
NSInteger index = sender.tag;
|
||||
if (index >= 0 && index < _zoomLevels.count) {
|
||||
CGFloat newZoomLevel = [_zoomLevels[index] floatValue];
|
||||
if (_zoomLevel != newZoomLevel) {
|
||||
[self setZoomLevel:newZoomLevel animated:true];
|
||||
self.zoomChanged(newZoomLevel, true, true);
|
||||
}
|
||||
} else {
|
||||
if (_zoomLevel != 1.0) {
|
||||
[self setZoomLevel:1.0 animated:true];
|
||||
self.zoomChanged(1.0, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)rightPressed {
|
||||
if (_zoomLevel != 2.0) {
|
||||
[self setZoomLevel:2.0 animated:true];
|
||||
self.zoomChanged(2.0, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,47 +393,63 @@
|
|||
- (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated
|
||||
{
|
||||
_zoomLevel = zoomLevel;
|
||||
if (zoomLevel < 1.0) {
|
||||
NSString *value = [NSString stringWithFormat:@"%.1f×", zoomLevel];
|
||||
value = [value stringByReplacingOccurrencesOfString:@"." withString:@","];
|
||||
if ([value isEqual:@"1,0×"] || [value isEqual:@"1×"]) {
|
||||
value = @"0,9×";
|
||||
}
|
||||
if (_leftItem.superview != nil) {
|
||||
[_leftItem setValue:value selected:true animated:animated];
|
||||
[_centerItem setValue:@"1" selected:false animated:animated];
|
||||
} else {
|
||||
[_centerItem setValue:value selected:false animated:animated];
|
||||
}
|
||||
[_rightItem setValue:@"2" selected:false animated:animated];
|
||||
} else if (zoomLevel < 2.0) {
|
||||
[_leftItem setValue:@"0,5" selected:false animated:animated];
|
||||
bool selected = _hasTelephotoCamera && _hasUltrawideCamera;
|
||||
if ((zoomLevel - 1.0) < 0.025) {
|
||||
[_centerItem setValue:@"1×" selected:true animated:animated];
|
||||
} else {
|
||||
NSString *value = [NSString stringWithFormat:@"%.1f×", zoomLevel];
|
||||
value = [value stringByReplacingOccurrencesOfString:@"." withString:@","];
|
||||
value = [value stringByReplacingOccurrencesOfString:@",0×" withString:@"×"];
|
||||
if ([value isEqual:@"2×"]) {
|
||||
value = @"1,9×";
|
||||
}
|
||||
[_centerItem setValue:value selected:selected animated:animated];
|
||||
}
|
||||
[_rightItem setValue:@"2" selected:false animated:animated];
|
||||
} else {
|
||||
[_leftItem setValue:@"0,5" selected:false animated:animated];
|
||||
|
||||
NSString *value = [[NSString stringWithFormat:@"%.1f×", zoomLevel] stringByReplacingOccurrencesOfString:@"." withString:@","];
|
||||
value = [value stringByReplacingOccurrencesOfString:@",0×" withString:@"×"];
|
||||
|
||||
NSInteger selectedIndex = -1;
|
||||
CGFloat closestDistance = MAXFLOAT;
|
||||
|
||||
CGFloat nextLevelValue = 8.0;
|
||||
for (NSInteger i = 0; i < _zoomLevels.count; i++) {
|
||||
CGFloat levelValue = [_zoomLevels[i] floatValue];
|
||||
CGFloat distance = ABS(levelValue - zoomLevel);
|
||||
|
||||
if (_rightItem.superview != nil) {
|
||||
[_centerItem setValue:@"1" selected:false animated:animated];
|
||||
[_rightItem setValue:value selected:true animated:animated];
|
||||
if (distance < 0.025) {
|
||||
selectedIndex = i;
|
||||
break;
|
||||
} else {
|
||||
[_centerItem setValue:value selected:true animated:animated];
|
||||
if (zoomLevel >= levelValue) {
|
||||
selectedIndex = i;
|
||||
closestDistance = distance;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedIndex < _zoomLevels.count - 1) {
|
||||
nextLevelValue = [_zoomLevels[selectedIndex + 1] floatValue];
|
||||
} else {
|
||||
nextLevelValue = 16.0;
|
||||
}
|
||||
|
||||
for (NSInteger i = 0; i < _zoomItems.count; i++) {
|
||||
TGCameraZoomModeItemView *item = _zoomItems[i];
|
||||
NSNumber *levelNumber = _zoomLevels[i];
|
||||
CGFloat level = [levelNumber floatValue];
|
||||
|
||||
NSString *value;
|
||||
if (level == (NSInteger)level && level >= 1.0) {
|
||||
value = [NSString stringWithFormat:@"%d", (int)level];
|
||||
} else {
|
||||
value = [NSString stringWithFormat:@"%.1f", level];
|
||||
value = [value stringByReplacingOccurrencesOfString:@"," withString:@"."];
|
||||
value = [value stringByReplacingOccurrencesOfString:@".0" withString:@""];
|
||||
}
|
||||
|
||||
bool selected = (i == selectedIndex);
|
||||
if (i == selectedIndex && closestDistance < MAXFLOAT && closestDistance > 0.025) {
|
||||
NSString *currentValue = [NSString stringWithFormat:@"%.1f", zoomLevel];
|
||||
currentValue = [currentValue stringByReplacingOccurrencesOfString:@"," withString:@"."];
|
||||
currentValue = [currentValue stringByReplacingOccurrencesOfString:@".0" withString:@""];
|
||||
if ([currentValue isEqualToString:[NSString stringWithFormat:@"%d", (int)nextLevelValue]]) {
|
||||
currentValue = [NSString stringWithFormat:@"%.1f", nextLevelValue - 0.1];
|
||||
}
|
||||
value = currentValue;
|
||||
}
|
||||
if (selected) {
|
||||
value = [NSString stringWithFormat:@"%@×", value];
|
||||
}
|
||||
[item setValue:value selected:selected animated:animated];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHidden:(BOOL)hidden
|
||||
|
|
@ -495,29 +485,22 @@
|
|||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
if (_leftItem.superview == nil && _rightItem.superview == nil) {
|
||||
_backgroundView.frame = CGRectMake(43, 0, 43, 43);
|
||||
} else if (_leftItem.superview != nil && _rightItem.superview == nil) {
|
||||
_backgroundView.frame = CGRectMake(21 + TGScreenPixel, 0, 86, 43);
|
||||
_leftItem.frame = CGRectMake(21 + TGScreenPixel, 0, 43, 43);
|
||||
_centerItem.frame = CGRectMake(21 + TGScreenPixel + 43, 0, 43, 43);
|
||||
} else if (_leftItem.superview == nil && _rightItem.superview != nil) {
|
||||
_backgroundView.frame = CGRectMake(21 + TGScreenPixel, 0, 86, 43);
|
||||
_centerItem.frame = CGRectMake(21 + TGScreenPixel, 0, 43, 43);
|
||||
_rightItem.frame = CGRectMake(21 + TGScreenPixel + 43, 0, 43, 43);
|
||||
} else {
|
||||
_leftItem.frame = CGRectMake(0, 0, 43, 43.0);
|
||||
_centerItem.frame = CGRectMake(43, 0, 43, 43.0);
|
||||
_rightItem.frame = CGRectMake(86, 0, 43, 43.0);
|
||||
CGFloat itemWidth = 43.0;
|
||||
CGFloat totalWidth = itemWidth * _zoomLevels.count;
|
||||
_backgroundView.frame = CGRectMake(TGScreenPixelFloor((172.0f - totalWidth) / 2.0), 0, totalWidth, 43);
|
||||
|
||||
for (NSInteger i = 0; i < _zoomItems.count; i++) {
|
||||
TGCameraZoomModeItemView *item = _zoomItems[i];
|
||||
item.frame = CGRectMake(_backgroundView.frame.origin.x + i * itemWidth, 0, itemWidth, 43);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
|
||||
{
|
||||
_interfaceOrientation = interfaceOrientation;
|
||||
_leftItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation));
|
||||
_centerItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation));
|
||||
_rightItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation));
|
||||
for (TGCameraZoomModeItemView *item in _zoomItems) {
|
||||
item.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation));
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -949,6 +949,8 @@
|
|||
NSInteger num = 0;
|
||||
bool grouping = selectionContext.grouping;
|
||||
|
||||
bool isHighQualityPhoto = editingContext.isHighQualityPhoto;
|
||||
|
||||
NSNumber *price;
|
||||
bool hasAnyTimers = false;
|
||||
if (editingContext != nil || grouping)
|
||||
|
|
@ -1071,9 +1073,11 @@
|
|||
if (price != nil)
|
||||
dict[@"price"] = price;
|
||||
|
||||
if (spoiler) {
|
||||
if (spoiler)
|
||||
dict[@"spoiler"] = @true;
|
||||
}
|
||||
|
||||
if (isHighQualityPhoto)
|
||||
dict[@"hd"] = @true;
|
||||
|
||||
id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier);
|
||||
return generatedItem;
|
||||
|
|
@ -1202,6 +1206,9 @@
|
|||
if (adjustments.paintingData.stickers.count > 0)
|
||||
dict[@"stickers"] = adjustments.paintingData.stickers;
|
||||
|
||||
if (isHighQualityPhoto)
|
||||
dict[@"hd"] = @true;
|
||||
|
||||
bool animated = adjustments.paintingData.hasAnimation;
|
||||
if (animated) {
|
||||
dict[@"isAnimation"] = @true;
|
||||
|
|
|
|||
|
|
@ -131,10 +131,13 @@
|
|||
SPipe *_fullSizePipe;
|
||||
SPipe *_cropPipe;
|
||||
SPipe *_captionAbovePipe;
|
||||
SPipe *_highQualityPhotoPipe;
|
||||
|
||||
NSAttributedString *_forcedCaption;
|
||||
|
||||
bool _captionAbove;
|
||||
|
||||
bool _highQualityPhoto;
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
@ -209,6 +212,7 @@
|
|||
_fullSizePipe = [[SPipe alloc] init];
|
||||
_cropPipe = [[SPipe alloc] init];
|
||||
_captionAbovePipe = [[SPipe alloc] init];
|
||||
_highQualityPhotoPipe = [[SPipe alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
@ -888,6 +892,29 @@
|
|||
_captionAbovePipe.sink(@(captionAbove));
|
||||
}
|
||||
|
||||
- (bool)isHighQualityPhoto {
|
||||
return _highQualityPhoto;
|
||||
}
|
||||
|
||||
- (SSignal *)highQualityPhoto
|
||||
{
|
||||
__weak TGMediaEditingContext *weakSelf = self;
|
||||
SSignal *updateSignal = [_highQualityPhotoPipe.signalProducer() map:^NSNumber *(NSNumber *update)
|
||||
{
|
||||
__strong TGMediaEditingContext *strongSelf = weakSelf;
|
||||
return @(strongSelf->_highQualityPhoto);
|
||||
}];
|
||||
|
||||
return [[SSignal single:@(_highQualityPhoto)] then:updateSignal];
|
||||
}
|
||||
|
||||
- (void)setHighQualityPhoto:(bool)highQualityPhoto
|
||||
{
|
||||
_highQualityPhoto = highQualityPhoto;
|
||||
_highQualityPhotoPipe.sink(@(highQualityPhoto));
|
||||
}
|
||||
|
||||
|
||||
- (SSignal *)facesForItem:(NSObject<TGMediaEditableItem> *)item
|
||||
{
|
||||
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
|
||||
|
|
|
|||
|
|
@ -598,6 +598,14 @@
|
|||
return [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab];
|
||||
}
|
||||
|
||||
- (UIView *)qualityButton
|
||||
{
|
||||
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
|
||||
return [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
else
|
||||
return [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
}
|
||||
|
||||
- (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel
|
||||
{
|
||||
_selectedPhotosView.selectedItemsModel = selectedItemsModel;
|
||||
|
|
@ -1100,33 +1108,43 @@
|
|||
TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
if (qualityButton != nil)
|
||||
{
|
||||
TGMediaVideoConversionPreset preset = 0;
|
||||
TGMediaVideoConversionPreset adjustmentsPreset = TGMediaVideoConversionPresetCompressedDefault;
|
||||
if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]])
|
||||
adjustmentsPreset = ((TGMediaVideoEditAdjustments *)adjustments).preset;
|
||||
|
||||
if (adjustmentsPreset != TGMediaVideoConversionPresetCompressedDefault)
|
||||
{
|
||||
preset = adjustmentsPreset;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSNumber *presetValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"TG_preferredVideoPreset_v0"];
|
||||
if (presetValue != nil)
|
||||
preset = (TGMediaVideoConversionPreset)[presetValue integerValue];
|
||||
bool isPhoto = [_currentItemView isKindOfClass:[TGMediaPickerGalleryPhotoItemView class]];
|
||||
if (isPhoto) {
|
||||
bool isHd = _editingContext.isHighQualityPhoto;
|
||||
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:isHd filled: false];
|
||||
qualityButton.iconImage = icon;
|
||||
|
||||
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
qualityButton.iconImage = icon;
|
||||
} else {
|
||||
TGMediaVideoConversionPreset preset = 0;
|
||||
TGMediaVideoConversionPreset adjustmentsPreset = TGMediaVideoConversionPresetCompressedDefault;
|
||||
if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]])
|
||||
adjustmentsPreset = ((TGMediaVideoEditAdjustments *)adjustments).preset;
|
||||
|
||||
if (adjustmentsPreset != TGMediaVideoConversionPresetCompressedDefault)
|
||||
{
|
||||
preset = adjustmentsPreset;
|
||||
}
|
||||
else
|
||||
preset = TGMediaVideoConversionPresetCompressedMedium;
|
||||
{
|
||||
NSNumber *presetValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"TG_preferredVideoPreset_v0"];
|
||||
if (presetValue != nil)
|
||||
preset = (TGMediaVideoConversionPreset)[presetValue integerValue];
|
||||
else
|
||||
preset = TGMediaVideoConversionPresetCompressedMedium;
|
||||
}
|
||||
|
||||
TGMediaVideoConversionPreset bestPreset = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions];
|
||||
if (preset > bestPreset)
|
||||
preset = bestPreset;
|
||||
|
||||
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForPreset:preset];
|
||||
qualityButton.iconImage = icon;
|
||||
|
||||
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
qualityButton.iconImage = icon;
|
||||
}
|
||||
|
||||
TGMediaVideoConversionPreset bestPreset = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions];
|
||||
if (preset > bestPreset)
|
||||
preset = bestPreset;
|
||||
|
||||
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForPreset:preset];
|
||||
qualityButton.iconImage = icon;
|
||||
|
||||
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
qualityButton.iconImage = icon;
|
||||
}
|
||||
|
||||
TGPhotoEditorButton *timerButton = [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab];
|
||||
|
|
@ -1207,6 +1225,25 @@
|
|||
[_tooltipContainerView hideMenu];
|
||||
}
|
||||
|
||||
- (void)showPhotoQualityTooltip:(bool)hd {
|
||||
[self updateEditorButtonsForItem:_currentItem animated:false];
|
||||
|
||||
UIView *button = [self qualityButton];
|
||||
CGRect rect = [button convertRect:button.bounds toView:nil];
|
||||
[self showPhotoQualityTooltip:rect hd:hd];
|
||||
}
|
||||
|
||||
- (void)showPhotoQualityTooltip:(CGRect)rect hd:(bool)hd
|
||||
{
|
||||
if (_tooltipContainerView != nil) {
|
||||
[self tooltipTimerTick];
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
NSString *text = hd ? @"The photo will be sent in high quality." : @"The photo will be sent in standard quality.";
|
||||
[_context presentTooltip:text icon:[TGPhotoEditorInterfaceAssets qualityIconForHighQuality:hd filled: true] sourceRect:rect];
|
||||
}
|
||||
|
||||
#pragma mark - Grouping Tooltip
|
||||
|
||||
- (void)actionStageActionRequested:(NSString *)action options:(id)__unused options
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#import "TGModernGalleryEditableItemView.h"
|
||||
#import "TGMediaPickerGalleryItem.h"
|
||||
#import <LegacyComponents/TGModernGalleryZoomableItemView.h>
|
||||
#import "TGMediaPickerGalleryPhotoItem.h"
|
||||
#import "TGMediaPickerGalleryVideoItem.h"
|
||||
#import "TGMediaPickerGalleryVideoItemView.h"
|
||||
|
||||
|
|
@ -195,8 +196,14 @@
|
|||
return;
|
||||
|
||||
__strong TGModernGalleryController *controller = strongSelf.controller;
|
||||
if ([controller.currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)])
|
||||
[strongSelf presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)controller.currentItem tab:tab];
|
||||
if ([controller.currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)]) {
|
||||
if (tab == TGPhotoEditorQualityTab && [controller.currentItem isKindOfClass:[TGMediaPickerGalleryFetchResultItem class]] && [((TGMediaPickerGalleryFetchResultItem *)controller.currentItem).backingItem isKindOfClass:[TGMediaPickerGalleryPhotoItem class]]) {
|
||||
[strongSelf->_editingContext setHighQualityPhoto:!strongSelf->_editingContext.isHighQualityPhoto];
|
||||
[strongSelf->_interfaceView showPhotoQualityTooltip:strongSelf->_editingContext.isHighQualityPhoto];
|
||||
} else {
|
||||
[strongSelf presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)controller.currentItem tab:tab];
|
||||
}
|
||||
}
|
||||
}];
|
||||
_interfaceView.photoStripItemSelected = ^(NSInteger index)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
- (TGPhotoEditorTab)toolbarTabs
|
||||
{
|
||||
return TGPhotoEditorCropTab | TGPhotoEditorToolsTab | TGPhotoEditorPaintTab;
|
||||
return TGPhotoEditorCropTab | TGPhotoEditorToolsTab | TGPhotoEditorPaintTab | TGPhotoEditorQualityTab;
|
||||
}
|
||||
|
||||
- (Class)viewClass
|
||||
|
|
|
|||
|
|
@ -215,6 +215,48 @@
|
|||
return backgroundImage;
|
||||
}
|
||||
|
||||
+ (UIImage *)qualityIconForHighQuality:(bool)highQuality filled:(bool)filled
|
||||
{
|
||||
CGFloat lineWidth = 2.0f - TGScreenPixel;
|
||||
|
||||
CGSize size = CGSizeMake(26.0f, 22.0f);
|
||||
CGRect rect = CGRectInset(CGRectMake(0.0f, 0.0f, size.width, size.height), lineWidth / 2.0, lineWidth / 2.0);
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0f);
|
||||
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:5.0f];
|
||||
|
||||
NSString *label = highQuality ? @"HD" : @"SD";
|
||||
|
||||
CGContextAddPath(context, path.CGPath);
|
||||
if (filled) {
|
||||
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
|
||||
CGContextFillPath(context);
|
||||
} else {
|
||||
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
|
||||
CGContextSetLineWidth(context, lineWidth);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
if (filled) {
|
||||
CGContextSetBlendMode(context, kCGBlendModeClear);
|
||||
}
|
||||
|
||||
UIFont *font = [TGFont roundedFontOfSize:11];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
CGSize textSize = [label sizeWithFont:font];
|
||||
[[UIColor whiteColor] setFill];
|
||||
[label drawInRect:CGRectMake((size.width - textSize.width) / 2.0f + TGScreenPixel, 4.0f, textSize.width, textSize.height) withFont:font];
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (UIImage *)qualityIconForPreset:(TGMediaVideoConversionPreset)preset
|
||||
{
|
||||
CGFloat lineWidth = 2.0f - TGScreenPixel;
|
||||
|
|
|
|||
|
|
@ -140,14 +140,16 @@ private final class LegacyAssetItemWrapper: NSObject {
|
|||
let timer: Int?
|
||||
let spoiler: Bool?
|
||||
let price: Int64?
|
||||
let forceHd: Bool
|
||||
let groupedId: Int64?
|
||||
let uniqueId: String?
|
||||
|
||||
init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, price: Int64?, groupedId: Int64?, uniqueId: String?) {
|
||||
init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, price: Int64?, forceHd: Bool = false, groupedId: Int64?, uniqueId: String?) {
|
||||
self.item = item
|
||||
self.timer = timer
|
||||
self.spoiler = spoiler
|
||||
self.price = price
|
||||
self.forceHd = forceHd
|
||||
self.groupedId = groupedId
|
||||
self.uniqueId = uniqueId
|
||||
|
||||
|
|
@ -202,10 +204,10 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str
|
|||
if let customName = dict["fileName"] as? String {
|
||||
name = customName
|
||||
}
|
||||
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
} else {
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
let forceHd = (dict["hd"] as? NSNumber)?.boolValue ?? false
|
||||
result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, price: price, forceHd: forceHd, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId)
|
||||
}
|
||||
return result
|
||||
} else if (dict["type"] as! NSString) == "file" {
|
||||
|
|
@ -578,7 +580,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
|
|||
arc4random_buf(&randomId, 8)
|
||||
let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight))
|
||||
let scaledSize = size.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0))
|
||||
let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max))
|
||||
let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max), forceHd: item.forceHd)
|
||||
|
||||
let media: Media
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ swift_library(
|
|||
"//submodules/DeviceAccess:DeviceAccess",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/Camera",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
"//submodules/TooltipUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import Display
|
|||
import SwiftSignalKit
|
||||
import LegacyComponents
|
||||
import TelegramPresentationData
|
||||
import Camera
|
||||
import DeviceModel
|
||||
import TooltipUI
|
||||
|
||||
public enum LegacyControllerPresentation {
|
||||
case custom
|
||||
|
|
@ -193,6 +196,11 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
|
|||
public func forceStatusBarAppearanceUpdate() {
|
||||
}
|
||||
|
||||
public func cameraZoomLevels() -> [NSNumber]! {
|
||||
let metrics = Camera.Metrics(model: DeviceModel.current)
|
||||
return metrics.zoomLevels.map { NSNumber(value: $0) }
|
||||
}
|
||||
|
||||
public func currentlyInSplitView() -> Bool {
|
||||
if let controller = self.controller as? LegacyController, let validLayout = controller.validLayout {
|
||||
return validLayout.isNonExclusive
|
||||
|
|
@ -258,6 +266,25 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
|
|||
|
||||
}
|
||||
|
||||
public func presentTooltip(_ text: String!, icon: UIImage!, sourceRect: CGRect) {
|
||||
guard let context = legacyContextGet() else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = TooltipScreen(
|
||||
account: context.account,
|
||||
sharedContext: context.sharedContext,
|
||||
text: .plain(text: text),
|
||||
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
|
||||
icon: .image(icon),
|
||||
location: .point(sourceRect, .bottom),
|
||||
displayDuration: .custom(2.0),
|
||||
shouldDismissOnTouch: { _, _ in
|
||||
return .dismiss(consume: false)
|
||||
})
|
||||
context.sharedContext.presentGlobalController(controller, nil)
|
||||
}
|
||||
|
||||
public func makeOverlayWindowManager() -> LegacyComponentsOverlayWindowManager! {
|
||||
return LegacyComponentsOverlayWindowManagerImpl(parentController: self.controller, theme: self.theme)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ extension UIImage.Orientation {
|
|||
|
||||
private let fetchPhotoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2)
|
||||
|
||||
public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, height: Int32?, format: MediaImageFormat?, quality: Int32?, useExif: Bool) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
|
||||
public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, height: Int32?, format: MediaImageFormat?, quality: Int32?, hd: Bool, useExif: Bool) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
|
||||
return Signal { subscriber in
|
||||
let queue = ThreadPoolQueue(threadPool: fetchPhotoWorkers)
|
||||
|
||||
|
|
@ -121,7 +121,11 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he
|
|||
if let width, let height {
|
||||
size = CGSize(width: CGFloat(width), height: CGFloat(height))
|
||||
} else {
|
||||
size = CGSize(width: 1280.0, height: 1280.0)
|
||||
if hd {
|
||||
size = CGSize(width: 2560.0, height: 2560.0)
|
||||
} else {
|
||||
size = CGSize(width: 1280.0, height: 1280.0)
|
||||
}
|
||||
}
|
||||
|
||||
var targetSize = PHImageManagerMaximumSize
|
||||
|
|
|
|||
|
|
@ -214,6 +214,74 @@ public final class LocalFileVideoMediaResource: TelegramMediaResource {
|
|||
}
|
||||
}
|
||||
|
||||
public struct LocalFileAudioMediaResourceId {
|
||||
public let randomId: Int64
|
||||
|
||||
public var uniqueId: String {
|
||||
return "lad-\(self.randomId)"
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.randomId.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
public final class LocalFileAudioMediaResource: TelegramMediaResource {
|
||||
public var size: Int64? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public let randomId: Int64
|
||||
public let path: String
|
||||
public let trimRange: Range<Double>?
|
||||
|
||||
public var headerSize: Int32 {
|
||||
return 32 * 1024
|
||||
}
|
||||
|
||||
public init(randomId: Int64, path: String, trimRange: Range<Double>?) {
|
||||
self.randomId = randomId
|
||||
self.path = path
|
||||
self.trimRange = trimRange
|
||||
}
|
||||
|
||||
public required init(decoder: PostboxDecoder) {
|
||||
self.randomId = decoder.decodeInt64ForKey("i", orElse: 0)
|
||||
self.path = decoder.decodeStringForKey("p", orElse: "")
|
||||
|
||||
if let trimLowerBound = decoder.decodeOptionalDoubleForKey("tl"), let trimUpperBound = decoder.decodeOptionalDoubleForKey("tu") {
|
||||
self.trimRange = trimLowerBound ..< trimUpperBound
|
||||
} else {
|
||||
self.trimRange = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.randomId, forKey: "i")
|
||||
encoder.encodeString(self.path, forKey: "p")
|
||||
|
||||
if let trimRange = self.trimRange {
|
||||
encoder.encodeDouble(trimRange.lowerBound, forKey: "tl")
|
||||
encoder.encodeDouble(trimRange.upperBound, forKey: "tu")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "tl")
|
||||
encoder.encodeNil(forKey: "tu")
|
||||
}
|
||||
}
|
||||
|
||||
public var id: MediaResourceId {
|
||||
return MediaResourceId(LocalFileAudioMediaResourceId(randomId: self.randomId).uniqueId)
|
||||
}
|
||||
|
||||
public func isEqual(to: MediaResource) -> Bool {
|
||||
if let to = to as? LocalFileAudioMediaResource {
|
||||
return self.randomId == to.randomId && self.path == to.path && self.trimRange == to.trimRange
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct PhotoLibraryMediaResourceId {
|
||||
public let localIdentifier: String
|
||||
public let resourceId: Int64
|
||||
|
|
@ -247,14 +315,16 @@ public class PhotoLibraryMediaResource: TelegramMediaResource {
|
|||
public let height: Int32?
|
||||
public let format: MediaImageFormat?
|
||||
public let quality: Int32?
|
||||
public let forceHd: Bool
|
||||
|
||||
public init(localIdentifier: String, uniqueId: Int64, width: Int32? = nil, height: Int32? = nil, format: MediaImageFormat? = nil, quality: Int32? = nil) {
|
||||
public init(localIdentifier: String, uniqueId: Int64, width: Int32? = nil, height: Int32? = nil, format: MediaImageFormat? = nil, quality: Int32? = nil, forceHd: Bool = false) {
|
||||
self.localIdentifier = localIdentifier
|
||||
self.uniqueId = uniqueId
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.format = format
|
||||
self.quality = quality
|
||||
self.forceHd = forceHd
|
||||
}
|
||||
|
||||
public required init(decoder: PostboxDecoder) {
|
||||
|
|
@ -264,6 +334,7 @@ public class PhotoLibraryMediaResource: TelegramMediaResource {
|
|||
self.height = decoder.decodeOptionalInt32ForKey("h")
|
||||
self.format = decoder.decodeOptionalInt32ForKey("f").flatMap(MediaImageFormat.init(rawValue:))
|
||||
self.quality = decoder.decodeOptionalInt32ForKey("q")
|
||||
self.forceHd = decoder.decodeBoolForKey("hd", orElse: false)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
|
|
@ -289,6 +360,7 @@ public class PhotoLibraryMediaResource: TelegramMediaResource {
|
|||
} else {
|
||||
encoder.encodeNil(forKey: "q")
|
||||
}
|
||||
encoder.encodeBool(self.forceHd, forKey: "hd")
|
||||
}
|
||||
|
||||
public var id: MediaResourceId {
|
||||
|
|
@ -315,6 +387,9 @@ public class PhotoLibraryMediaResource: TelegramMediaResource {
|
|||
if self.quality != to.quality {
|
||||
return false
|
||||
}
|
||||
if self.forceHd != to.forceHd {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -2717,6 +2717,19 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
|||
self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {})
|
||||
})))
|
||||
}
|
||||
if price == nil {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Send in High Quality", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualityHd"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
if let editingContext = self?.interaction?.editingState {
|
||||
editingContext.setHighQualityPhoto(true)
|
||||
}
|
||||
self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {})
|
||||
})))
|
||||
}
|
||||
if selectionCount > 1, price == nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Attachment_SendWithoutGrouping, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/GroupingOff"), color: theme.contextMenu.primaryColor)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import AsyncDisplayKit
|
|||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
|
||||
public enum MediaPlayerTimeTextNodeMode {
|
||||
case normal
|
||||
case reversed
|
||||
|
|
@ -54,12 +52,14 @@ private final class MediaPlayerTimeTextNodeParameters: NSObject {
|
|||
let alignment: NSTextAlignment
|
||||
let mode: MediaPlayerTimeTextNodeMode
|
||||
let textColor: UIColor
|
||||
let textFont: UIFont
|
||||
|
||||
init(state: MediaPlayerTimeTextNodeState, alignment: NSTextAlignment, mode: MediaPlayerTimeTextNodeMode, textColor: UIColor) {
|
||||
init(state: MediaPlayerTimeTextNodeState, alignment: NSTextAlignment, mode: MediaPlayerTimeTextNodeMode, textColor: UIColor, textFont: UIFont) {
|
||||
self.state = state
|
||||
self.alignment = alignment
|
||||
self.mode = mode
|
||||
self.textColor = textColor
|
||||
self.textFont = textFont
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
|
@ -76,12 +76,27 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||
self.updateTimestamp()
|
||||
}
|
||||
}
|
||||
|
||||
public var textFont: UIFont {
|
||||
didSet {
|
||||
self.updateTimestamp()
|
||||
}
|
||||
}
|
||||
|
||||
public var defaultDuration: Double? {
|
||||
didSet {
|
||||
self.updateTimestamp()
|
||||
}
|
||||
}
|
||||
|
||||
public var trimRange: Range<Double>? {
|
||||
didSet {
|
||||
self.updateTimestamp()
|
||||
}
|
||||
}
|
||||
|
||||
public var showDurationIfNotStarted = false
|
||||
|
||||
private var updateTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var statusValue: MediaPlayerStatus? {
|
||||
|
|
@ -118,8 +133,9 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
public init(textColor: UIColor) {
|
||||
public init(textColor: UIColor, textFont: UIFont = Font.regular(13.0)) {
|
||||
self.textColor = textColor
|
||||
self.textFont = textFont
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -157,24 +173,38 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||
if ((self.statusValue?.duration ?? 0.0) < 0.1) && self.state.seconds != nil && self.keepPreviousValueOnEmptyState {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) {
|
||||
let timestampSeconds: Double
|
||||
if !statusValue.generationTimestamp.isZero {
|
||||
timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp)
|
||||
} else {
|
||||
timestampSeconds = statusValue.timestamp
|
||||
let timestamp = statusValue.timestamp - (self.trimRange?.lowerBound ?? 0.0)
|
||||
var duration = statusValue.duration
|
||||
if let trimRange = self.trimRange {
|
||||
duration = trimRange.upperBound - trimRange.lowerBound
|
||||
}
|
||||
switch self.mode {
|
||||
|
||||
if self.showDurationIfNotStarted && timestamp < .ulpOfOne {
|
||||
let timestamp = Int32(duration)
|
||||
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
|
||||
} else {
|
||||
let timestampSeconds: Double
|
||||
if !statusValue.generationTimestamp.isZero {
|
||||
timestampSeconds = timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp)
|
||||
} else {
|
||||
timestampSeconds = timestamp
|
||||
}
|
||||
switch self.mode {
|
||||
case .normal:
|
||||
let timestamp = Int32(truncatingIfNeeded: Int64(floor(timestampSeconds)))
|
||||
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
|
||||
case .reversed:
|
||||
let timestamp = abs(Int32(Int32(truncatingIfNeeded: Int64(floor(timestampSeconds - statusValue.duration)))))
|
||||
let timestamp = abs(Int32(Int32(truncatingIfNeeded: Int64(floor(timestampSeconds - duration)))))
|
||||
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
|
||||
}
|
||||
}
|
||||
} else if let defaultDuration = self.defaultDuration {
|
||||
let timestamp = Int32(defaultDuration)
|
||||
var timestamp = Int32(defaultDuration)
|
||||
if let trimRange = self.trimRange {
|
||||
timestamp = Int32(trimRange.upperBound - trimRange.lowerBound)
|
||||
}
|
||||
self.state = MediaPlayerTimeTextNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
|
||||
} else {
|
||||
self.state = MediaPlayerTimeTextNodeState()
|
||||
|
|
@ -190,7 +220,7 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor)
|
||||
return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor, textFont: self.textFont)
|
||||
}
|
||||
|
||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
|
|
@ -203,7 +233,7 @@ public final class MediaPlayerTimeTextNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
if let parameters = parameters as? MediaPlayerTimeTextNodeParameters {
|
||||
let string = NSAttributedString(string: parameters.state.string, font: textFont, textColor: parameters.textColor)
|
||||
let string = NSAttributedString(string: parameters.state.string, font: parameters.textFont, textColor: parameters.textColor)
|
||||
let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size
|
||||
if parameters.alignment == .left {
|
||||
string.draw(at: CGPoint())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
@interface TGDataItem : NSObject
|
||||
|
||||
- (instancetype)initWithData:(NSData *)data;
|
||||
- (void)appendData:(NSData *)data;
|
||||
- (NSData *)data;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (NSUInteger)encodedBytes;
|
||||
- (NSTimeInterval)encodedDuration;
|
||||
|
||||
- (NSDictionary *)pause;
|
||||
- (bool)resumeWithDataItem:(TGDataItem *)dataItem encoderState:(NSDictionary *)state;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@
|
|||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithData:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_data = [[NSMutableData alloc] init];
|
||||
[self appendData:data];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)appendData:(NSData *)data {
|
||||
[_data appendData:data];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -468,6 +468,138 @@ static inline int writeOggPage(ogg_page *page, TGDataItem *fileItem)
|
|||
return total_samples / (NSTimeInterval)coding_rate;
|
||||
}
|
||||
|
||||
- (NSDictionary *)pause
|
||||
{
|
||||
[self flushPages];
|
||||
return [self saveState];
|
||||
}
|
||||
|
||||
- (bool)resumeWithDataItem:(TGDataItem *)dataItem encoderState:(NSDictionary *)state
|
||||
{
|
||||
if (![self restoreState:state withDataItem:dataItem])
|
||||
return false;
|
||||
|
||||
_packetId++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)flushPages
|
||||
{
|
||||
while (ogg_stream_flush_fill(&os, &og, 255 * 255))
|
||||
{
|
||||
if (ogg_page_packets(&og) != 0)
|
||||
last_granulepos = ogg_page_granulepos(&og);
|
||||
|
||||
last_segments -= og.header[26];
|
||||
int writtenPageBytes = writeOggPage(&og, _dataItem);
|
||||
if (writtenPageBytes != og.header_len + og.body_len)
|
||||
{
|
||||
NSLog(@"Error: failed writing data to output stream");
|
||||
return false;
|
||||
}
|
||||
bytes_written += writtenPageBytes;
|
||||
pages_out++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
- (NSDictionary *)saveState
|
||||
{
|
||||
NSMutableDictionary *state = [NSMutableDictionary dictionary];
|
||||
|
||||
[state setObject:@(_packetId) forKey:@"packetId"];
|
||||
[state setObject:@(enc_granulepos) forKey:@"enc_granulepos"];
|
||||
[state setObject:@(last_granulepos) forKey:@"last_granulepos"];
|
||||
[state setObject:@(last_segments) forKey:@"last_segments"];
|
||||
[state setObject:@(nb_encoded) forKey:@"nb_encoded"];
|
||||
[state setObject:@(bytes_written) forKey:@"bytes_written"];
|
||||
[state setObject:@(pages_out) forKey:@"pages_out"];
|
||||
[state setObject:@(total_bytes) forKey:@"total_bytes"];
|
||||
[state setObject:@(total_samples) forKey:@"total_samples"];
|
||||
[state setObject:@(serialno) forKey:@"serialno"];
|
||||
|
||||
[state setObject:@(rate) forKey:@"rate"];
|
||||
[state setObject:@(coding_rate) forKey:@"coding_rate"];
|
||||
[state setObject:@(frame_size) forKey:@"frame_size"];
|
||||
[state setObject:@(bitrate) forKey:@"bitrate"];
|
||||
[state setObject:@(with_cvbr) forKey:@"with_cvbr"];
|
||||
[state setObject:@(lookahead) forKey:@"lookahead"];
|
||||
|
||||
NSDictionary *headerDict = @{
|
||||
@"channels": @(header.channels),
|
||||
@"channel_mapping": @(header.channel_mapping),
|
||||
@"input_sample_rate": @(header.input_sample_rate),
|
||||
@"gain": @(header.gain),
|
||||
@"nb_streams": @(header.nb_streams),
|
||||
@"preskip": @(header.preskip)
|
||||
};
|
||||
[state setObject:headerDict forKey:@"header"];
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
- (bool)restoreState:(NSDictionary *)state withDataItem:(TGDataItem *)dataItem
|
||||
{
|
||||
if (state == nil || dataItem == nil)
|
||||
return false;
|
||||
|
||||
[self cleanup];
|
||||
|
||||
_dataItem = dataItem;
|
||||
|
||||
_packetId = [state[@"packetId"] intValue];
|
||||
enc_granulepos = [state[@"enc_granulepos"] longLongValue];
|
||||
last_granulepos = [state[@"last_granulepos"] longLongValue];
|
||||
last_segments = [state[@"last_segments"] intValue];
|
||||
nb_encoded = [state[@"nb_encoded"] longLongValue];
|
||||
bytes_written = [state[@"bytes_written"] longLongValue];
|
||||
pages_out = [state[@"pages_out"] longLongValue];
|
||||
total_bytes = [state[@"total_bytes"] longLongValue];
|
||||
total_samples = [state[@"total_samples"] longLongValue];
|
||||
serialno = [state[@"serialno"] intValue];
|
||||
|
||||
rate = [state[@"rate"] intValue];
|
||||
coding_rate = [state[@"coding_rate"] intValue];
|
||||
frame_size = [state[@"frame_size"] intValue];
|
||||
bitrate = [state[@"bitrate"] intValue];
|
||||
with_cvbr = [state[@"with_cvbr"] intValue];
|
||||
lookahead = [state[@"lookahead"] intValue];
|
||||
|
||||
NSDictionary *headerDict = state[@"header"];
|
||||
header.channels = [headerDict[@"channels"] intValue];
|
||||
header.channel_mapping = [headerDict[@"channel_mapping"] intValue];
|
||||
header.input_sample_rate = [headerDict[@"input_sample_rate"] intValue];
|
||||
header.gain = [headerDict[@"gain"] intValue];
|
||||
header.nb_streams = [headerDict[@"nb_streams"] intValue];
|
||||
header.preskip = [headerDict[@"preskip"] intValue];
|
||||
|
||||
int result = OPUS_OK;
|
||||
_encoder = opus_encoder_create(coding_rate, header.channels, OPUS_APPLICATION_AUDIO, &result);
|
||||
if (result != OPUS_OK)
|
||||
{
|
||||
NSLog(@"Error cannot create encoder: %s", opus_strerror(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate));
|
||||
|
||||
#ifdef OPUS_SET_LSB_DEPTH
|
||||
opus_encoder_ctl(_encoder, OPUS_SET_LSB_DEPTH(16));
|
||||
#endif
|
||||
|
||||
if (ogg_stream_init(&os, serialno) == -1)
|
||||
{
|
||||
NSLog(@"Error: stream init failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
min_bytes = max_frame_bytes = (1275 * 3 + 7) * header.nb_streams;
|
||||
_packet = malloc(max_frame_bytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -333,6 +333,13 @@ public final class MediaBox {
|
|||
}
|
||||
}
|
||||
|
||||
public func moveResourceData(_ id: MediaResourceId, toTempPath: String) {
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(id)
|
||||
let _ = try? FileManager.default.moveItem(at: URL(fileURLWithPath: paths.complete), to: URL(fileURLWithPath: toTempPath))
|
||||
}
|
||||
}
|
||||
|
||||
public func copyResourceData(_ id: MediaResourceId, fromTempPath: String) {
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(id)
|
||||
|
|
@ -340,11 +347,11 @@ public final class MediaBox {
|
|||
}
|
||||
}
|
||||
|
||||
public func moveResourceData(from: MediaResourceId, to: MediaResourceId) {
|
||||
public func moveResourceData(from: MediaResourceId, to: MediaResourceId, synchronous: Bool = false) {
|
||||
if from == to {
|
||||
return
|
||||
}
|
||||
self.dataQueue.async {
|
||||
let begin = {
|
||||
let pathsFrom = self.storePathsForId(from)
|
||||
let pathsTo = self.storePathsForId(to)
|
||||
link(pathsFrom.partial, pathsTo.partial)
|
||||
|
|
@ -352,6 +359,11 @@ public final class MediaBox {
|
|||
unlink(pathsFrom.partial)
|
||||
unlink(pathsFrom.complete)
|
||||
}
|
||||
if synchronous {
|
||||
begin()
|
||||
} else {
|
||||
self.dataQueue.async(begin)
|
||||
}
|
||||
}
|
||||
|
||||
public func copyResourceData(from: MediaResourceId, to: MediaResourceId, synchronous: Bool = false) {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ public final class QrCodeScanScreen: ViewController {
|
|||
|
||||
public var showMyCode: () -> Void = {}
|
||||
public var completion: (String?) -> Void = { _ in }
|
||||
public var dismissed: (() -> Void)?
|
||||
|
||||
private var codeResolved = false
|
||||
|
||||
|
|
@ -133,8 +134,6 @@ public final class QrCodeScanScreen: ViewController {
|
|||
|
||||
if case .custom = subject {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
} else if case .peer = subject {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Contacts_QrCode_MyCode, style: .plain, target: self, action: #selector(self.myCodePressed))
|
||||
} else {
|
||||
#if DEBUG
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, target: self, action: #selector(self.testPressed))
|
||||
|
|
@ -910,6 +909,9 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie
|
|||
if controller is QrCodeScanScreen {
|
||||
return false
|
||||
}
|
||||
if controller is ChatQrCodeScreen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ public final class SelectablePeerNodeTheme {
|
|||
}
|
||||
|
||||
public final class SelectablePeerNode: ASDisplayNode {
|
||||
public enum StoryMode {
|
||||
case createStory
|
||||
case repostStory
|
||||
case repostMessage
|
||||
}
|
||||
|
||||
private let contextContainer: ContextControllerSourceNode
|
||||
private let avatarSelectionNode: ASImageNode
|
||||
private let avatarNodeContainer: ASDisplayNode
|
||||
|
|
@ -176,14 +182,32 @@ public final class SelectablePeerNode: ASDisplayNode {
|
|||
)
|
||||
}
|
||||
|
||||
public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, isMessage: Bool) {
|
||||
public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, storyMode: StoryMode) {
|
||||
self.peer = nil
|
||||
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.textNode.attributedText = NSAttributedString(string: isMessage ? strings.Share_RepostToStory : strings.Share_RepostStory, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center)
|
||||
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: .repostIcon, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad)
|
||||
let title: String
|
||||
let overrideImage: AvatarNodeImageOverride
|
||||
|
||||
switch storyMode {
|
||||
case .createStory:
|
||||
//TODO:localize
|
||||
title = "Post\nto Story"
|
||||
overrideImage = .storyIcon
|
||||
case .repostStory:
|
||||
title = strings.Share_RepostStory
|
||||
overrideImage = .repostIcon
|
||||
case .repostMessage:
|
||||
title = strings.Share_RepostToStory
|
||||
overrideImage = .repostIcon
|
||||
}
|
||||
|
||||
self.avatarNode.playRepostAnimation()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.textNode.attributedText = NSAttributedString(string: title, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center)
|
||||
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad)
|
||||
|
||||
if case .repostIcon = overrideImage {
|
||||
self.avatarNode.playRepostAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
|
||||
|
|
|
|||
|
|
@ -1573,7 +1573,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
|
||||
func updatePeers(context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, defaultAction: ShareControllerAction?) {
|
||||
self.context = context
|
||||
|
||||
|
||||
if let peersContentNode = self.peersContentNode, peersContentNode.accountPeer.id == accountPeer.id {
|
||||
peersContentNode.peersValue.set(.single(peers))
|
||||
return
|
||||
|
|
@ -1610,7 +1610,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
}
|
||||
|
||||
let animated = self.peersContentNode == nil
|
||||
let peersContentNode = SharePeersContainerNode(environment: self.environment, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in
|
||||
let peersContentNode = SharePeersContainerNode(environment: self.environment, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, isMainApp: self.environment.isMainApp, switchToAnotherAccount: { [weak self] in
|
||||
self?.switchToAnotherAccount?()
|
||||
}, debugAction: { [weak self] in
|
||||
self?.debugAction?()
|
||||
|
|
|
|||
|
|
@ -96,8 +96,14 @@ final class ShareControllerGridSectionNode: ASDisplayNode {
|
|||
|
||||
final class ShareControllerPeerGridItem: GridItem {
|
||||
enum ShareItem: Equatable {
|
||||
enum StoryMode {
|
||||
case createStory
|
||||
case repostStory
|
||||
case repostMessage
|
||||
}
|
||||
|
||||
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)
|
||||
case story(isMessage: Bool)
|
||||
case story(mode: StoryMode)
|
||||
|
||||
var peerId: EnginePeer.Id? {
|
||||
if case let .peer(peer, _, _, _, _, _) = self {
|
||||
|
|
@ -254,7 +260,16 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
self.placeholderNode = nil
|
||||
shimmerNode.removeFromSupernode()
|
||||
}
|
||||
} else if let item, case let .story(isMessage) = item {
|
||||
} else if let item, case let .story(mode) = item {
|
||||
let storyMode: SelectablePeerNode.StoryMode
|
||||
switch mode {
|
||||
case .createStory:
|
||||
storyMode = .createStory
|
||||
case .repostStory:
|
||||
storyMode = .repostStory
|
||||
case .repostMessage:
|
||||
storyMode = .repostMessage
|
||||
}
|
||||
self.peerNode.setupStoryRepost(
|
||||
accountPeerId: context.accountPeerId,
|
||||
postbox: context.stateManager.postbox,
|
||||
|
|
@ -262,7 +277,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
theme: theme,
|
||||
strings: strings,
|
||||
synchronousLoad: synchronousLoad,
|
||||
isMessage: isMessage
|
||||
storyMode: storyMode
|
||||
)
|
||||
} else {
|
||||
let shimmerNode: ShimmerEffectNode
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
}
|
||||
private let tick = ValuePromise<Int>(0)
|
||||
|
||||
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) {
|
||||
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool, requiresStars: Int64?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, isMainApp: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) {
|
||||
self.environment = environment
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
|
|
@ -171,7 +171,17 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
var index: Int32 = 0
|
||||
|
||||
if canShareStory {
|
||||
entries.append(SharePeerEntry(index: index, item: .story(isMessage: fromPublicChannel), theme: theme, strings: strings))
|
||||
let storyMode: ShareControllerPeerGridItem.ShareItem.StoryMode
|
||||
if fromPublicChannel {
|
||||
storyMode = .repostMessage
|
||||
} else {
|
||||
if !isMainApp {
|
||||
storyMode = .createStory
|
||||
} else {
|
||||
storyMode = .repostStory
|
||||
}
|
||||
}
|
||||
entries.append(SharePeerEntry(index: index, item: .story(mode: storyMode), theme: theme, strings: strings))
|
||||
index += 1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ public enum PermissionRequestStatus {
|
|||
self = .unreachable
|
||||
case .allowed:
|
||||
self = .allowed
|
||||
case .limited:
|
||||
self = .allowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -477,6 +477,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/CheckComponent",
|
||||
"//submodules/TelegramUI/Components/MarqueeComponent",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
|
||||
"//submodules/TelegramUI/Components/ForumSettingsScreen",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
|
|
|||
|
|
@ -517,6 +517,38 @@ private final class AdminUserActionsSheetComponent: Component {
|
|||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
if self.component == nil {
|
||||
let _ = (component.context.account.postbox.peerView(id: component.chatPeer.id)
|
||||
|> take(1)).start(next: { [weak self] peerView in
|
||||
guard let self else{
|
||||
return
|
||||
}
|
||||
|
||||
var selectAll = false
|
||||
if let cachedData = peerView.cachedData as? CachedChannelData {
|
||||
if let memberCount = cachedData.participantsSummary.memberCount, memberCount >= 1000 {
|
||||
selectAll = true
|
||||
} else if case let .known(peerId) = cachedData.linkedDiscussionPeerId, let _ = peerId {
|
||||
selectAll = true
|
||||
} else if case let .channel(channel) = component.chatPeer, let _ = channel.addressName {
|
||||
selectAll = true
|
||||
}
|
||||
}
|
||||
|
||||
if selectAll {
|
||||
var selectedPeers = Set<EnginePeer.Id>()
|
||||
for peer in component.peers {
|
||||
selectedPeers.insert(peer.peer.id)
|
||||
}
|
||||
self.optionReportSelectedPeers = selectedPeers
|
||||
self.optionDeleteAllSelectedPeers = selectedPeers
|
||||
self.optionBanSelectedPeers = selectedPeers
|
||||
}
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated()
|
||||
}
|
||||
})
|
||||
|
||||
var (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([])
|
||||
if case let .channel(channel) = component.chatPeer {
|
||||
(allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights(channel.defaultBannedRights?.flags ?? [])
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ swift_library(
|
|||
"//submodules/SegmentedControlNode",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
"//submodules/HexColor",
|
||||
"//submodules/QrCodeUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import SaveToCameraRoll
|
|||
import SegmentedControlNode
|
||||
import AnimatedCountLabelNode
|
||||
import HexColor
|
||||
import QrCodeUI
|
||||
|
||||
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
|
|
@ -536,7 +537,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
|
|||
snapshotView.frame = self.containerNode.view.frame
|
||||
self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: ChatQrCodeScreenImpl.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
|
@ -561,7 +562,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
public final class ChatQrCodeScreen: ViewController {
|
||||
public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
|
||||
public static let themeCrossfadeDuration: Double = 0.3
|
||||
public static let themeCrossfadeDelay: Double = 0.05
|
||||
|
||||
|
|
@ -603,7 +604,7 @@ public final class ChatQrCodeScreen: ViewController {
|
|||
private var animatedIn = false
|
||||
|
||||
private let context: AccountContext
|
||||
fileprivate let subject: ChatQrCodeScreen.Subject
|
||||
fileprivate let subject: ChatQrCodeScreenImpl.Subject
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationThemePromise = Promise<PresentationTheme?>()
|
||||
|
|
@ -611,13 +612,15 @@ public final class ChatQrCodeScreen: ViewController {
|
|||
|
||||
public var dismissed: (() -> Void)?
|
||||
|
||||
public init(context: AccountContext, subject: ChatQrCodeScreen.Subject) {
|
||||
public init(context: AccountContext, subject: ChatQrCodeScreenImpl.Subject) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.subject = subject
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
|
|
@ -664,14 +667,11 @@ public final class ChatQrCodeScreen: ViewController {
|
|||
}
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
self.controllerNode.cancel = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.dismiss()
|
||||
strongSelf.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -690,19 +690,25 @@ public final class ChatQrCodeScreen: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
self.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
self.controllerNode.animateOut(completion: completion)
|
||||
|
||||
if flag {
|
||||
self.controllerNode.animateOut(completion: {
|
||||
super.dismiss(animated: false, completion: completion)
|
||||
})
|
||||
} else {
|
||||
super.dismiss(animated: flag, completion: completion)
|
||||
}
|
||||
|
||||
self.dismissed?()
|
||||
}
|
||||
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
|
@ -754,7 +760,7 @@ private func generateShadowImage() -> UIImage? {
|
|||
private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private weak var controller: ChatQrCodeScreen?
|
||||
private weak var controller: ChatQrCodeScreenImpl?
|
||||
|
||||
private let contentNode: ContentNode
|
||||
|
||||
|
|
@ -772,6 +778,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
private let animationContainerNode: ASDisplayNode
|
||||
private var animationNode: AnimationNode
|
||||
private let doneButton: SolidRoundedButtonNode
|
||||
private let scanButton: SolidRoundedButtonNode
|
||||
|
||||
private let listNode: ListView
|
||||
private var entries: [ThemeSettingsThemeEntry]?
|
||||
|
|
@ -805,10 +812,9 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
var present: ((ViewController) -> Void)?
|
||||
var previewTheme: ((String?, Bool?, PresentationTheme) -> Void)?
|
||||
var completion: ((String?) -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
var cancel: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreen) {
|
||||
init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreenImpl) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.presentationData = presentationData
|
||||
|
|
@ -817,7 +823,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
|
||||
|
||||
switch controller.subject {
|
||||
case let .peer(peer, threadId, temporary):
|
||||
self.contentNode = QrContentNode(context: context, peer: peer, threadId: threadId, isStatic: false, temporary: temporary)
|
||||
|
|
@ -888,6 +894,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
self.doneButton.title = self.presentationData.strings.Share_ShareMessage
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
self.scanButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor), font: .regular, height: 42.0, cornerRadius: 0.0, gloss: false)
|
||||
self.scanButton.title = "Scan QR Code"
|
||||
self.scanButton.icon = UIImage(bundleImageName: "Settings/ScanQr")
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
|
|
@ -910,6 +921,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
self.contentContainerNode.addSubnode(self.titleNode)
|
||||
self.contentContainerNode.addSubnode(self.segmentedNode)
|
||||
self.contentContainerNode.addSubnode(self.doneButton)
|
||||
self.contentContainerNode.addSubnode(self.scanButton)
|
||||
|
||||
self.topContentContainerNode.addSubnode(self.animationContainerNode)
|
||||
self.animationContainerNode.addSubnode(self.animationNode)
|
||||
|
|
@ -992,6 +1004,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
}
|
||||
}
|
||||
|
||||
self.scanButton.pressed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = QrCodeScanScreen(context: self.context, subject: .peer)
|
||||
self.controller?.push(controller)
|
||||
}
|
||||
|
||||
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
||||
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
||||
|
|
@ -1246,9 +1266,6 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
|
||||
private var switchThemeIconAnimator: DisplayLinkAnimator?
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
guard !self.animatedOut else {
|
||||
return
|
||||
}
|
||||
let previousTheme = self.presentationData.theme
|
||||
self.presentationData = presentationData
|
||||
|
||||
|
|
@ -1261,6 +1278,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
|
||||
self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
|
||||
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
|
||||
self.scanButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor))
|
||||
|
||||
let previousIconColors = iconColors(theme: previousTheme)
|
||||
let newIconColors = iconColors(theme: self.presentationData.theme)
|
||||
|
|
@ -1336,7 +1354,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
if let snapshotView = self.contentNode.containerNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
self.contentNode.view.insertSubview(snapshotView, aboveSubview: self.contentNode.containerNode.view)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: ChatQrCodeScreenImpl.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
|
@ -1345,14 +1363,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
snapshotView.frame = self.animationNode.frame
|
||||
self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: ChatQrCodeScreenImpl.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(ChatQrCodeScreen.themeCrossfadeDelay) {
|
||||
Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) {
|
||||
if let effectView = self.effectNode.view as? UIVisualEffectView {
|
||||
UIView.animate(withDuration: ChatQrCodeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) {
|
||||
UIView.animate(withDuration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) {
|
||||
effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
|
||||
} completion: { _ in
|
||||
}
|
||||
|
|
@ -1360,14 +1378,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
|
||||
let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear
|
||||
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
|
||||
self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreen.themeCrossfadeDuration)
|
||||
self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration)
|
||||
}
|
||||
|
||||
if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = self.contentContainerNode.frame
|
||||
self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: ChatQrCodeScreenImpl.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
|
@ -1395,11 +1413,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
self.animatedOut = true
|
||||
|
||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
||||
self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss?()
|
||||
completion?()
|
||||
}
|
||||
self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1421,7 +1436,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
|
||||
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
let titleHeight: CGFloat = 54.0
|
||||
let contentHeight = titleHeight + bottomInset + 188.0
|
||||
let contentHeight = titleHeight + bottomInset + 188.0 + 52.0
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
|
||||
|
||||
|
|
@ -1459,9 +1474,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
|||
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
|
||||
|
||||
let buttonInset: CGFloat = 16.0
|
||||
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight))
|
||||
let scanButtonHeight = self.scanButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: scanButtonHeight))
|
||||
|
||||
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight))
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame)
|
||||
|
||||
|
|
|
|||
|
|
@ -1444,6 +1444,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
break
|
||||
case .stars:
|
||||
break
|
||||
case .shareStory:
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
34
submodules/TelegramUI/Components/ForumSettingsScreen/BUILD
Normal file
34
submodules/TelegramUI/Components/ForumSettingsScreen/BUILD
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ForumSettingsScreen",
|
||||
module_name = "ForumSettingsScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
import LottieComponent
|
||||
|
||||
final class ForumModeComponent: Component {
|
||||
enum Mode: Equatable {
|
||||
case tabs
|
||||
case list
|
||||
}
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let mode: Mode?
|
||||
let modeUpdated: (Mode) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
mode: Mode?,
|
||||
modeUpdated: @escaping (Mode) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.mode = mode
|
||||
self.modeUpdated = modeUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: ForumModeComponent, rhs: ForumModeComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.mode != rhs.mode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let tabs = ComponentView<Empty>()
|
||||
private let list = ComponentView<Empty>()
|
||||
|
||||
private var component: ForumModeComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ForumModeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 224.0)
|
||||
|
||||
let sideInset = (size.width - 160.0 * 2.0) / 2.0
|
||||
//TODO:localize
|
||||
let tabsSize = self.tabs.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
ItemComponent(
|
||||
theme: component.theme,
|
||||
animation: "ForumTabs",
|
||||
title: "Tabs",
|
||||
isSelected: component.mode == .tabs
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
component.modeUpdated(.tabs)
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: size.height)
|
||||
)
|
||||
if let tabsView = self.tabs.view {
|
||||
if tabsView.superview == nil {
|
||||
self.addSubview(tabsView)
|
||||
}
|
||||
transition.setFrame(view: tabsView, frame: CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: tabsSize))
|
||||
}
|
||||
|
||||
let listSize = self.list.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
ItemComponent(
|
||||
theme: component.theme,
|
||||
animation: "ForumList",
|
||||
title: "List",
|
||||
isSelected: component.mode == .list
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
component.modeUpdated(.list)
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: size.height)
|
||||
)
|
||||
if let listView = self.list.view {
|
||||
if listView.superview == nil {
|
||||
self.addSubview(listView)
|
||||
}
|
||||
transition.setFrame(view: listView, frame: CGRect(origin: CGPoint(x: sideInset + tabsSize.width, y: 0.0), size: listSize))
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let animation: String
|
||||
let title: String
|
||||
let isSelected: Bool
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
animation: String,
|
||||
title: String,
|
||||
isSelected: Bool
|
||||
) {
|
||||
self.theme = theme
|
||||
self.animation = animation
|
||||
self.title = title
|
||||
self.isSelected = isSelected
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.animation != rhs.animation {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.isSelected != rhs.isSelected {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let animation = ComponentView<Empty>()
|
||||
private let selection = ComponentView<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var component: ItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 224.0)
|
||||
|
||||
let animationSize = self.animation.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: component.animation),
|
||||
color: component.isSelected ? component.theme.list.itemCheckColors.fillColor : component.theme.list.itemSecondaryTextColor,
|
||||
loop: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 170.0, height: 170.0)
|
||||
)
|
||||
if let animationView = self.animation.view {
|
||||
if animationView.superview == nil {
|
||||
self.addSubview(animationView)
|
||||
}
|
||||
transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - animationSize.width) / 2.0), y: 9.0), size: animationSize))
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.medium(13.0), textColor: component.isSelected ? component.theme.list.itemCheckColors.foregroundColor : component.theme.list.itemPrimaryTextColor)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - 23.0), size: titleSize)
|
||||
|
||||
let selectionFrame = titleFrame.insetBy(dx: -10.0, dy: -6.0)
|
||||
let _ = self.selection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: selectionFrame.size.height / 2.0)),
|
||||
environment: {},
|
||||
containerSize: selectionFrame.size
|
||||
)
|
||||
if let selectionView = self.selection.view {
|
||||
if selectionView.superview == nil {
|
||||
self.addSubview(selectionView)
|
||||
}
|
||||
transition.setFrame(view: selectionView, frame: selectionFrame)
|
||||
selectionView.alpha = component.isSelected ? 1.0 : 0.0
|
||||
}
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
|
||||
if previousComponent?.isSelected != component.isSelected && component.isSelected {
|
||||
if let animationView = self.animation.view as? LottieComponent.View {
|
||||
animationView.playOnce()
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,470 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Photos
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import ListSectionComponent
|
||||
import ListActionItemComponent
|
||||
import BundleIconComponent
|
||||
import LottieComponent
|
||||
import PlainButtonComponent
|
||||
import TelegramStringFormatting
|
||||
import Markdown
|
||||
|
||||
final class ForumSettingsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
}
|
||||
|
||||
static func ==(lhs: ForumSettingsScreenComponent, rhs: ForumSettingsScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private let navigationTitle = ComponentView<Empty>()
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let generalSection = ComponentView<Empty>()
|
||||
private let modeSection = ComponentView<Empty>()
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: ForumSettingsScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var peer: EnginePeer?
|
||||
private var peerDisposable: Disposable?
|
||||
|
||||
private var isOn = false
|
||||
private var mode: ForumModeComponent.Mode = .tabs
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peerDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
var scrolledUp = true
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
let navigationRevealOffsetY: CGFloat = 0.0
|
||||
|
||||
let navigationAlphaDistance: CGFloat = 16.0
|
||||
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
|
||||
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
|
||||
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
|
||||
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
|
||||
}
|
||||
|
||||
var scrolledUp = false
|
||||
if navigationAlpha < 0.5 {
|
||||
scrolledUp = true
|
||||
} else if navigationAlpha > 0.5 {
|
||||
scrolledUp = false
|
||||
}
|
||||
|
||||
if self.scrolledUp != scrolledUp {
|
||||
self.scrolledUp = scrolledUp
|
||||
if !self.isUpdating {
|
||||
self.state?.updated()
|
||||
}
|
||||
}
|
||||
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ForumSettingsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
self.peerDisposable = (component.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.peer = peer
|
||||
if case let .channel(channel) = peer {
|
||||
self.isOn = channel.flags.contains(.isForum)
|
||||
self.mode = channel.flags.contains(.displayForumAsTabs) ? .tabs : .list
|
||||
}
|
||||
self.state?.updated()
|
||||
})
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? transition : transition.withAnimation(.curve(duration: 0.25, curve: .easeInOut))
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
//TODO:localize
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Topics", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
if navigationTitleView.superview == nil {
|
||||
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
|
||||
navigationBar.view.addSubview(navigationTitleView)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
|
||||
}
|
||||
|
||||
let bottomContentInset: CGFloat = 24.0
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let sectionSpacing: CGFloat = 32.0
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
contentHeight += environment.navigationHeight
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "Topics"),
|
||||
loop: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 8.0), size: iconSize)
|
||||
if let iconView = self.icon.view as? LottieComponent.View {
|
||||
if iconView.superview == nil {
|
||||
self.scrollView.addSubview(iconView)
|
||||
iconView.playOnce()
|
||||
}
|
||||
transition.setPosition(view: iconView, position: iconFrame.center)
|
||||
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
||||
}
|
||||
|
||||
contentHeight += 124.0
|
||||
|
||||
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("The group chat will be divided into topics created by admins or users.", attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
|
||||
linkAttribute: { attributes in
|
||||
return ("URL", "")
|
||||
}), textAlignment: .center
|
||||
))
|
||||
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(subtitleString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.25,
|
||||
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||
return NSAttributedString.Key(rawValue: "URL")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { [weak self] _, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let _ = component
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||
)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
self.scrollView.addSubview(subtitleView)
|
||||
}
|
||||
transition.setPosition(view: subtitleView, position: subtitleFrame.center)
|
||||
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
|
||||
}
|
||||
contentHeight += subtitleSize.height
|
||||
contentHeight += 27.0
|
||||
|
||||
var generalSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
generalSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Enable Topics",
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isOn, action: { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.isOn = !self.isOn
|
||||
let displayForumAsTabs = self.mode == .tabs
|
||||
let _ = component.context.engine.peers.setChannelForumMode(id: component.peerId, isForum: self.isOn, displayForumAsTabs: displayForumAsTabs).startStandalone()
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
})),
|
||||
action: nil
|
||||
))))
|
||||
|
||||
let generalSectionSize = self.generalSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: generalSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let generalSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: generalSectionSize)
|
||||
if let generalSectionView = self.generalSection.view {
|
||||
if generalSectionView.superview == nil {
|
||||
self.scrollView.addSubview(generalSectionView)
|
||||
}
|
||||
transition.setFrame(view: generalSectionView, frame: generalSectionFrame)
|
||||
}
|
||||
contentHeight += generalSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
|
||||
var otherSectionsHeight: CGFloat = 0.0
|
||||
|
||||
let modeSectionSize = self.modeSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "DISPLAY AS",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(
|
||||
text: "Choose how topics appear for all members.",
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor),
|
||||
linkAttribute: { _ in
|
||||
return nil
|
||||
}
|
||||
)
|
||||
),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(
|
||||
id: 0,
|
||||
component: AnyComponent(
|
||||
ForumModeComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
mode: self.isOn ? self.mode : nil,
|
||||
modeUpdated: { [weak self] mode in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.mode = mode
|
||||
let displayForumAsTabs = self.mode == .tabs
|
||||
let _ = component.context.engine.peers.setChannelForumMode(id: component.peerId, isForum: true, displayForumAsTabs: displayForumAsTabs).startStandalone()
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let modeSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + otherSectionsHeight), size: modeSectionSize)
|
||||
if let modeSectionView = self.modeSection.view {
|
||||
if modeSectionView.superview == nil {
|
||||
modeSectionView.layer.allowsGroupOpacity = true
|
||||
self.scrollView.addSubview(modeSectionView)
|
||||
}
|
||||
transition.setFrame(view: modeSectionView, frame: modeSectionFrame)
|
||||
alphaTransition.setAlpha(view: modeSectionView, alpha: self.isOn ? 1.0 : 0.0)
|
||||
}
|
||||
otherSectionsHeight += modeSectionSize.height
|
||||
otherSectionsHeight += sectionSpacing
|
||||
|
||||
if self.isOn {
|
||||
contentHeight += otherSectionsHeight
|
||||
}
|
||||
|
||||
contentHeight += bottomContentInset
|
||||
contentHeight += environment.safeInsets.bottom
|
||||
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
self.ignoreScrolling = true
|
||||
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
||||
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
if bounds.maxY != previousBounds.maxY {
|
||||
let offsetY = previousBounds.maxY - bounds.maxY
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ForumSettingsScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: ForumSettingsScreenComponent(
|
||||
context: context,
|
||||
peerId: peerId
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.title = ""
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? ForumSettingsScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
componentView.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -1359,7 +1359,8 @@ final class GiftSetupScreenComponent: Component {
|
|||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0
|
||||
cornerRadius: 10.0,
|
||||
isShimmering: true
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(buttonString),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public extension MediaEditorScreenImpl {
|
|||
if let image = UIImage(contentsOfFile: data.path) {
|
||||
return .single(nil)
|
||||
|> then(
|
||||
.single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||
.single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|
||||
|> delay(0.1, queue: Queue.mainQueue())
|
||||
)
|
||||
} else {
|
||||
|
|
@ -56,7 +56,7 @@ public extension MediaEditorScreenImpl {
|
|||
}
|
||||
return .single(nil)
|
||||
|> then(
|
||||
.single(.video(videoPath: symlinkPath, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: PixelDimensions(width: 720, height: 1280), duration: duration ?? 0.0, videoPositionChanges: [], additionalVideoPosition: .bottomRight))
|
||||
.single(.video(videoPath: symlinkPath, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: PixelDimensions(width: 720, height: 1280), duration: duration ?? 0.0, videoPositionChanges: [], additionalVideoPosition: .bottomRight, fromCamera: false))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1358,7 +1358,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
|
||||
var canRecordVideo = true
|
||||
if let subject = controller.node.subject {
|
||||
if case let .video(_, _, _, additionalPath, _, _, _, _, _) = subject, additionalPath != nil {
|
||||
if case let .video(_, _, _, additionalPath, _, _, _, _, _, _) = subject, additionalPath != nil {
|
||||
canRecordVideo = false
|
||||
}
|
||||
if case .videoCollage = subject {
|
||||
|
|
@ -2348,8 +2348,8 @@ final class MediaEditorScreenComponent: Component {
|
|||
)
|
||||
|
||||
var selectedItemId = ""
|
||||
if case let .asset(asset) = controller.node.subject {
|
||||
selectedItemId = asset.localIdentifier
|
||||
if let subject = controller.node.subject, let item = controller.node.items.first(where: { $0.source.identifier == subject.sourceIdentifier }) {
|
||||
selectedItemId = item.identifier
|
||||
}
|
||||
|
||||
let selectionPanel: ComponentView<Empty>
|
||||
|
|
@ -2385,7 +2385,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) {
|
||||
if let itemIndex = controller.node.items.firstIndex(where: { $0.identifier == id }) {
|
||||
controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
|
|
@ -2394,7 +2394,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else {
|
||||
guard let fromIndex = controller.node.items.firstIndex(where: { $0.identifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.identifier == toId }), toIndex < controller.node.items.count else {
|
||||
return
|
||||
}
|
||||
let fromItem = controller.node.items[fromIndex]
|
||||
|
|
@ -2928,19 +2928,87 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
}
|
||||
|
||||
struct EditingItem: Equatable {
|
||||
let asset: PHAsset
|
||||
enum Source: Equatable {
|
||||
case image(UIImage, PixelDimensions)
|
||||
case video(String, UIImage?, PixelDimensions, Double)
|
||||
case asset(PHAsset)
|
||||
|
||||
static func ==(lhs: Source, rhs: Source) -> Bool {
|
||||
switch lhs {
|
||||
case let .image(lhsImage, _):
|
||||
if case let .image(rhsImage, _) = rhs {
|
||||
return lhsImage === rhsImage
|
||||
}
|
||||
case let .video(lhsPath, _, _, _):
|
||||
if case let .video(rhsPath, _, _, _) = rhs {
|
||||
return lhsPath == rhsPath
|
||||
}
|
||||
case let .asset(lhsAsset):
|
||||
if case let .asset(rhsAsset) = rhs {
|
||||
return lhsAsset.localIdentifier == rhsAsset.localIdentifier
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var identifier: String {
|
||||
switch self {
|
||||
case let .image(image, _):
|
||||
return "\(Unmanaged.passUnretained(image).toOpaque())"
|
||||
case let .video(videoPath, _, _, _):
|
||||
return videoPath
|
||||
case let .asset(asset):
|
||||
return asset.localIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
var subject: MediaEditorScreenImpl.Subject {
|
||||
switch self {
|
||||
case let .image(image, dimensions):
|
||||
return .image(image: image, dimensions: dimensions, additionalImage: nil, additionalImagePosition: .bottomLeft, fromCamera: false)
|
||||
case let .video(videoPath, thumbnail, dimensions, duration):
|
||||
return .video(videoPath: videoPath, thumbnail: thumbnail, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: dimensions, duration: duration, videoPositionChanges: [], additionalVideoPosition: .bottomLeft, fromCamera: false)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
}
|
||||
}
|
||||
|
||||
var isVideo: Bool {
|
||||
switch self {
|
||||
case .image:
|
||||
return false
|
||||
case .video:
|
||||
return true
|
||||
case let .asset(asset):
|
||||
return asset.mediaType == .video
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var identifier: String
|
||||
var source: Source
|
||||
var values: MediaEditorValues?
|
||||
var caption = NSAttributedString()
|
||||
var thumbnail: UIImage?
|
||||
var isEnabled = true
|
||||
var version: Int = 0
|
||||
|
||||
init(asset: PHAsset) {
|
||||
self.asset = asset
|
||||
init?(subject: MediaEditorScreenImpl.Subject) {
|
||||
self.identifier = "\(Int64.random(in: 0 ..< .max))"
|
||||
switch subject {
|
||||
case let .image(image, dimensions, _, _, _):
|
||||
self.source = .image(image, dimensions)
|
||||
case let .video(videoPath, thumbnail, _, _, _, dimensions, duration, _, _, _):
|
||||
self.source = .video(videoPath, thumbnail, dimensions, duration)
|
||||
case let .asset(asset):
|
||||
self.source = .asset(asset)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: EditingItem, rhs: EditingItem) -> Bool {
|
||||
if lhs.asset.localIdentifier != rhs.asset.localIdentifier {
|
||||
if lhs.source != rhs.source {
|
||||
return false
|
||||
}
|
||||
if lhs.values != rhs.values {
|
||||
|
|
@ -3235,9 +3303,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
|
||||
var effectiveSubject = subject
|
||||
switch subject {
|
||||
case let .assets(assets):
|
||||
effectiveSubject = .asset(assets.first!)
|
||||
self.items = assets.map { EditingItem(asset: $0) }
|
||||
case let .multiple(subjects):
|
||||
effectiveSubject = subjects.first!
|
||||
self.items = subjects.compactMap { EditingItem(subject: $0) }
|
||||
case let .draft(draft, _):
|
||||
for entity in draft.values.entities {
|
||||
if case let .sticker(sticker) = entity {
|
||||
|
|
@ -3415,9 +3483,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
var isFromCamera = false
|
||||
let isSavingAvailable: Bool
|
||||
switch subject {
|
||||
case .image, .video:
|
||||
case let .image(_, _, _, _, fromCamera):
|
||||
isFromCamera = fromCamera
|
||||
isSavingAvailable = !controller.isEmbeddedEditor
|
||||
case let .video(_, _, _, _, _, _, _, _, _, fromCamera):
|
||||
isFromCamera = fromCamera
|
||||
isSavingAvailable = !controller.isEmbeddedEditor
|
||||
isFromCamera = true
|
||||
case .draft, .message,. gift:
|
||||
isSavingAvailable = true
|
||||
default:
|
||||
|
|
@ -3599,7 +3670,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
}
|
||||
|
||||
switch subject {
|
||||
case let .image(_, _, additionalImage, position):
|
||||
case let .image(_, _, additionalImage, position, _):
|
||||
if let additionalImage {
|
||||
let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
|
|
@ -3617,7 +3688,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
imageEntity.position = position.getPosition(storyDimensions)
|
||||
self.entitiesView.add(imageEntity, announce: false)
|
||||
}
|
||||
case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position):
|
||||
case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position, _):
|
||||
mediaEditor.setVideoIsMirrored(mirror)
|
||||
if let additionalVideoPath {
|
||||
let videoEntity = DrawingStickerEntity(content: .dualVideoReference(false))
|
||||
|
|
@ -3784,7 +3855,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
self.backgroundDimView.isHidden = false
|
||||
})
|
||||
}
|
||||
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject {
|
||||
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject, self.items.isEmpty {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
|
||||
|
|
@ -4427,7 +4498,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
case .camera:
|
||||
self.componentHostView?.animateIn(from: .camera, completion: completion)
|
||||
|
||||
if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage {
|
||||
if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition, _) = subject, let mainTransitionImage {
|
||||
var transitionImage = mainTransitionImage
|
||||
if let additionalTransitionImage {
|
||||
var backgroundImage = mainTransitionImage
|
||||
|
|
@ -5448,7 +5519,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
}
|
||||
|
||||
func switchToItem(_ identifier: String) {
|
||||
guard let controller = self.controller, let mediaEditor = self.mediaEditor, let itemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == identifier }), case let .asset(asset) = self.subject, let currentItemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) else {
|
||||
guard let controller = self.controller, let mediaEditor = self.mediaEditor, let itemIndex = self.items.firstIndex(where: { $0.identifier == identifier }), let subject = self.subject, let currentItemIndex = self.items.firstIndex(where: { $0.source.identifier == subject.sourceIdentifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -5489,7 +5560,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
|
||||
let targetItem = self.items[itemIndex]
|
||||
controller.node.setup(
|
||||
subject: .asset(targetItem.asset),
|
||||
subject: targetItem.source.subject,
|
||||
values: targetItem.values,
|
||||
caption: targetItem.caption
|
||||
)
|
||||
|
|
@ -6448,27 +6519,40 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
}
|
||||
|
||||
case empty(PixelDimensions)
|
||||
case image(image: UIImage, dimensions: PixelDimensions, additionalImage: UIImage?, additionalImagePosition: PIPPosition)
|
||||
case video(videoPath: String, thumbnail: UIImage?, mirror: Bool, additionalVideoPath: String?, additionalThumbnail: UIImage?, dimensions: PixelDimensions, duration: Double, videoPositionChanges: [(Bool, Double)], additionalVideoPosition: PIPPosition)
|
||||
case image(image: UIImage, dimensions: PixelDimensions, additionalImage: UIImage?, additionalImagePosition: PIPPosition, fromCamera: Bool)
|
||||
case video(videoPath: String, thumbnail: UIImage?, mirror: Bool, additionalVideoPath: String?, additionalThumbnail: UIImage?, dimensions: PixelDimensions, duration: Double, videoPositionChanges: [(Bool, Double)], additionalVideoPosition: PIPPosition, fromCamera: Bool)
|
||||
case videoCollage(items: [VideoCollageItem])
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft, Int64?)
|
||||
case message([MessageId])
|
||||
case gift(StarGift.UniqueGift)
|
||||
case sticker(TelegramMediaFile, [String])
|
||||
case assets([PHAsset])
|
||||
case multiple([Subject])
|
||||
|
||||
var sourceIdentifier: String {
|
||||
switch self {
|
||||
case let .image(image, _, _, _, _):
|
||||
return "\(Unmanaged.passUnretained(image).toOpaque())"
|
||||
case let .video(videoPath, _, _, _, _, _, _, _, _, _):
|
||||
return videoPath
|
||||
case let .asset(asset):
|
||||
return asset.localIdentifier
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
case let .empty(dimensions):
|
||||
return dimensions
|
||||
case let .image(_, dimensions, _, _), let .video(_, _, _, _, _, dimensions, _, _, _):
|
||||
case let .image(_, dimensions, _, _, _), let .video(_, _, _, _, _, dimensions, _, _, _, _):
|
||||
return dimensions
|
||||
case let .asset(asset):
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
case let .draft(draft, _):
|
||||
return draft.dimensions
|
||||
case .message, .gift, .sticker, .videoCollage, .assets:
|
||||
case .message, .gift, .sticker, .videoCollage, .multiple:
|
||||
return PixelDimensions(storyDimensions)
|
||||
}
|
||||
}
|
||||
|
|
@ -6480,9 +6564,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
return .image(image, dimensions)
|
||||
case let .image(image, dimensions, _, _):
|
||||
case let .image(image, dimensions, _, _, _):
|
||||
return .image(image, dimensions)
|
||||
case let .video(videoPath, transitionImage, mirror, additionalVideoPath, _, dimensions, duration, _, _):
|
||||
case let .video(videoPath, transitionImage, mirror, additionalVideoPath, _, dimensions, duration, _, _, _):
|
||||
return .video(videoPath, transitionImage, mirror, additionalVideoPath, dimensions, duration)
|
||||
case let .videoCollage(items):
|
||||
return .videoCollage(items.map { $0.editorItem })
|
||||
|
|
@ -6496,8 +6580,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
return .gift(gift)
|
||||
case let .sticker(sticker, _):
|
||||
return .sticker(sticker)
|
||||
case let .assets(assets):
|
||||
return .asset(assets.first!)
|
||||
case let .multiple(subjects):
|
||||
return subjects.first!.editorSubject
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6525,7 +6609,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
return false
|
||||
case .sticker:
|
||||
return false
|
||||
case .assets:
|
||||
case .multiple:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -8113,7 +8197,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
exportSubject = .single(.image(image: image))
|
||||
case let .video(path, _, _, _, _, _, _, _, _):
|
||||
case let .video(path, _, _, _, _, _, _, _, _, _):
|
||||
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
|
||||
exportSubject = .single(.video(asset: asset, isStory: true))
|
||||
case let .videoCollage(items):
|
||||
|
|
@ -8164,7 +8248,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
|> map { asset in
|
||||
return .video(asset: asset, isStory: true)
|
||||
}
|
||||
case let .image(image, _, _, _):
|
||||
case let .image(image, _, _, _, _):
|
||||
exportSubject = .single(.image(image: image))
|
||||
case let .asset(asset):
|
||||
exportSubject = Signal { subscriber in
|
||||
|
|
@ -8222,7 +8306,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
|||
}
|
||||
case let .sticker(file, _):
|
||||
exportSubject = .single(.sticker(file: file))
|
||||
case .assets:
|
||||
case .multiple:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ extension MediaEditorScreenImpl {
|
|||
if case .coverEditor = self.mode {
|
||||
return false
|
||||
}
|
||||
if case .assets = self.node.actualSubject {
|
||||
if case .multiple = self.node.actualSubject {
|
||||
return false
|
||||
}
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
|
|
@ -208,11 +208,11 @@ extension MediaEditorScreenImpl {
|
|||
switch subject {
|
||||
case .empty:
|
||||
break
|
||||
case let .image(image, dimensions, _, _):
|
||||
case let .image(image, dimensions, _, _, _):
|
||||
if let draft = innerSaveDraft(media: .image(image: image, dimensions: dimensions)) {
|
||||
completion?(draft)
|
||||
}
|
||||
case let .video(path, _, _, _, _, dimensions, _, _, _):
|
||||
case let .video(path, _, _, _, _, dimensions, _, _, _, _):
|
||||
if let draft = innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) {
|
||||
completion?(draft)
|
||||
}
|
||||
|
|
@ -256,7 +256,7 @@ extension MediaEditorScreenImpl {
|
|||
}
|
||||
case .sticker:
|
||||
break
|
||||
case .assets:
|
||||
case .multiple:
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ extension MediaEditorScreenImpl {
|
|||
let end = values.videoTrimRange?.upperBound ?? (min(originalDuration, start + storyMaxCombinedVideoDuration))
|
||||
|
||||
for i in 0 ..< storyCount {
|
||||
guard var editingItem = EditingItem(subject: .asset(asset)) else {
|
||||
continue
|
||||
}
|
||||
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(end, start + storyMaxVideoDuration))
|
||||
|
||||
var editingItem = EditingItem(asset: asset)
|
||||
if i == 0 {
|
||||
editingItem.caption = self.node.getCaption()
|
||||
}
|
||||
|
|
@ -176,7 +177,7 @@ extension MediaEditorScreenImpl {
|
|||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .image(image, _, _, _):
|
||||
case let .image(image, _, _, _, _):
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
|
|
@ -185,7 +186,7 @@ extension MediaEditorScreenImpl {
|
|||
duration = 5.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _):
|
||||
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _, _):
|
||||
videoIsMirrored = mirror
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
|
|
@ -426,7 +427,7 @@ extension MediaEditorScreenImpl {
|
|||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case .assets:
|
||||
case .multiple:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
|
@ -528,7 +529,7 @@ extension MediaEditorScreenImpl {
|
|||
}
|
||||
|
||||
var items = items
|
||||
if !isLongVideo, let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
||||
if !isLongVideo, let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.source.identifier == subject.sourceIdentifier }) {
|
||||
var updatedCurrentItem = items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.node.getCaption()
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
|
|
@ -563,7 +564,7 @@ extension MediaEditorScreenImpl {
|
|||
let randomId = Int64.random(in: .min ... .max)
|
||||
order.append(randomId)
|
||||
|
||||
if item.asset.mediaType == .video {
|
||||
if item.source.isVideo {
|
||||
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
|
|
@ -573,7 +574,7 @@ extension MediaEditorScreenImpl {
|
|||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else if item.asset.mediaType == .image {
|
||||
} else {
|
||||
processImageItem(item: item, index: index, randomId: randomId) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
|
|
@ -583,8 +584,6 @@ extension MediaEditorScreenImpl {
|
|||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -610,13 +609,8 @@ extension MediaEditorScreenImpl {
|
|||
}
|
||||
|
||||
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
|
|
@ -636,12 +630,13 @@ extension MediaEditorScreenImpl {
|
|||
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in
|
||||
let process: (AVAsset?, MediaResult.VideoResult) -> Void = { [weak self] avAsset, videoResult in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let avAsset else {
|
||||
DispatchQueue.main.async {
|
||||
if let self {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
Queue.mainQueue().async {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -650,16 +645,16 @@ extension MediaEditorScreenImpl {
|
|||
if let videoTrimRange = item.values?.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
duration = min(avAsset.duration.seconds, storyMaxVideoDuration)
|
||||
}
|
||||
|
||||
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
Queue.mainQueue().async {
|
||||
if let cgImage {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
|
||||
|
|
@ -677,14 +672,14 @@ extension MediaEditorScreenImpl {
|
|||
if let coverImage = coverImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .video(
|
||||
video: .asset(localIdentifier: asset.localIdentifier),
|
||||
video: videoResult,
|
||||
coverImage: coverImage,
|
||||
values: itemMediaEditor.values,
|
||||
duration: duration,
|
||||
dimensions: itemMediaEditor.values.resultDimensions
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
caption: convertMarkdownToAttributes(item.caption),
|
||||
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
|
|
@ -704,11 +699,21 @@ extension MediaEditorScreenImpl {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch item.source {
|
||||
case let .video(videoPath, _, _, _):
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: videoPath))
|
||||
process(avAsset, .videoFile(path: videoPath))
|
||||
case let .asset(asset):
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
process(avAsset, .asset(localIdentifier: asset.localIdentifier))
|
||||
}
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
|
|
@ -726,56 +731,67 @@ extension MediaEditorScreenImpl {
|
|||
}
|
||||
}
|
||||
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
||||
let process: (UIImage?) -> Void = { [weak self] image in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let image {
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
|
||||
if itemMediaEditor.values.gradientColors == nil {
|
||||
itemMediaEditor.setGradientColors(mediaEditorGetGradientColors(from: image))
|
||||
}
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: .zero,
|
||||
textScale: 2.0
|
||||
) { resultImage in
|
||||
if let resultImage = resultImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: resultImage,
|
||||
dimensions: PixelDimensions(resultImage.size)
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
guard let image else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
return
|
||||
}
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
|
||||
if itemMediaEditor.values.gradientColors == nil {
|
||||
itemMediaEditor.setGradientColors(mediaEditorGetGradientColors(from: image))
|
||||
}
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: .zero,
|
||||
textScale: 2.0
|
||||
) { resultImage in
|
||||
if let resultImage = resultImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: resultImage,
|
||||
dimensions: PixelDimensions(resultImage.size)
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
|
||||
switch item.source {
|
||||
case let .image(image, _):
|
||||
process(image)
|
||||
case let .asset(asset):
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
Queue.mainQueue().async {
|
||||
process(image)
|
||||
}
|
||||
}
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -784,10 +800,21 @@ extension MediaEditorScreenImpl {
|
|||
if values?.videoTrimRange == nil {
|
||||
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
|
||||
}
|
||||
|
||||
let editorSubject: MediaEditor.Subject
|
||||
switch item.source {
|
||||
case let .image(image, dimensions):
|
||||
editorSubject = .image(image, dimensions)
|
||||
case let .video(videoPath, thumbnailImage, dimensions, duration):
|
||||
editorSubject = .video(videoPath, thumbnailImage, false, nil, dimensions, duration)
|
||||
case let .asset(asset):
|
||||
editorSubject = .asset(asset)
|
||||
}
|
||||
|
||||
return MediaEditor(
|
||||
context: self.context,
|
||||
mode: .default,
|
||||
subject: .asset(item.asset),
|
||||
subject: editorSubject,
|
||||
values: values,
|
||||
hasHistogram: false,
|
||||
isStandalone: true
|
||||
|
|
|
|||
|
|
@ -84,13 +84,20 @@ final class SelectionPanelComponent: Component {
|
|||
let previousItem = self.item
|
||||
self.item = item
|
||||
|
||||
if previousItem?.asset.localIdentifier != item.asset.localIdentifier || previousItem?.version != item.version {
|
||||
if previousItem?.identifier != item.identifier || previousItem?.version != item.version {
|
||||
let imageSignal: Signal<UIImage?, NoError>
|
||||
if let thumbnail = item.thumbnail {
|
||||
imageSignal = .single(thumbnail)
|
||||
self.imageNode.contentMode = .scaleAspectFill
|
||||
} else {
|
||||
imageSignal = assetImage(asset: item.asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true)
|
||||
switch item.source {
|
||||
case let .image(image, _):
|
||||
imageSignal = .single(image)
|
||||
case let .video(_, image, _, _):
|
||||
imageSignal = .single(image)
|
||||
case let .asset(asset):
|
||||
imageSignal = assetImage(asset: asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true)
|
||||
}
|
||||
self.imageNode.contentUpdated = { [weak self] image in
|
||||
if let self {
|
||||
if self.backgroundNode.image == nil {
|
||||
|
|
@ -249,8 +256,8 @@ final class SelectionPanelComponent: Component {
|
|||
self.reorderRecognizer?.isEnabled = true
|
||||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
if let itemView = self.item(at: location), let item = itemView.item, item.asset.localIdentifier != component.selectedItemId {
|
||||
component.itemTapped(item.asset.localIdentifier)
|
||||
if let itemView = self.item(at: location), let item = itemView.item, item.identifier != component.selectedItemId {
|
||||
component.itemTapped(item.identifier)
|
||||
} else {
|
||||
component.itemTapped(nil)
|
||||
}
|
||||
|
|
@ -346,12 +353,12 @@ final class SelectionPanelComponent: Component {
|
|||
|
||||
let mappedPosition = self.convert(targetPosition, to: self.scrollView)
|
||||
|
||||
if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.asset.localIdentifier {
|
||||
if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.identifier {
|
||||
for (_, visibleItem) in self.itemViews {
|
||||
if visibleItem === visibleReorderingItem {
|
||||
continue
|
||||
}
|
||||
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.asset.localIdentifier {
|
||||
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.identifier {
|
||||
component.itemReordered(fromId, toId)
|
||||
break
|
||||
}
|
||||
|
|
@ -381,7 +388,7 @@ final class SelectionPanelComponent: Component {
|
|||
let mainCircleDelay: Double = 0.02
|
||||
let backgroundWidth = self.backgroundMaskPanelView.frame.width
|
||||
for item in component.items {
|
||||
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
|
||||
guard let itemView = self.itemViews[item.identifier] else {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -433,7 +440,7 @@ final class SelectionPanelComponent: Component {
|
|||
let backgroundWidth = self.backgroundMaskPanelView.frame.width
|
||||
|
||||
for item in component.items {
|
||||
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
|
||||
guard let itemView = self.itemViews[item.identifier] else {
|
||||
continue
|
||||
}
|
||||
let distance = abs(itemView.frame.center.x - backgroundWidth)
|
||||
|
|
@ -478,7 +485,7 @@ final class SelectionPanelComponent: Component {
|
|||
|
||||
var index = 1
|
||||
for item in component.items {
|
||||
let id = item.asset.localIdentifier
|
||||
let id = item.identifier
|
||||
validIds.insert(id)
|
||||
|
||||
var itemTransition = transition
|
||||
|
|
@ -498,7 +505,7 @@ final class SelectionPanelComponent: Component {
|
|||
}
|
||||
component.itemSelectionToggled(id)
|
||||
}
|
||||
itemView.update(item: item, number: index, isSelected: item.asset.localIdentifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition)
|
||||
itemView.update(item: item, number: index, isSelected: item.identifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition)
|
||||
|
||||
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size))
|
||||
itemTransition.setPosition(view: itemView, position: itemFrame.center)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func getWeather(context: AccountContext, load: Bool) -> Signal<StickerPickerScre
|
|||
switch status {
|
||||
case .notDetermined:
|
||||
return .single(.notDetermined)
|
||||
case .denied, .restricted, .unreachable:
|
||||
case .denied, .restricted, .unreachable, .limited:
|
||||
return .single(.notAllowed)
|
||||
case .allowed:
|
||||
if load {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ public final class MediaScrubberComponent: Component {
|
|||
public enum Style {
|
||||
case editor
|
||||
case videoMessage
|
||||
case voiceMessage
|
||||
case cover
|
||||
}
|
||||
|
||||
|
|
@ -519,7 +520,7 @@ public final class MediaScrubberComponent: Component {
|
|||
switch component.style {
|
||||
case .editor:
|
||||
self.cursorView.isHidden = false
|
||||
case .videoMessage:
|
||||
case .videoMessage, .voiceMessage:
|
||||
self.cursorView.isHidden = true
|
||||
case .cover:
|
||||
self.cursorView.isHidden = false
|
||||
|
|
@ -748,7 +749,7 @@ public final class MediaScrubberComponent: Component {
|
|||
switch component.style {
|
||||
case .editor, .cover:
|
||||
fullTrackHeight = trackHeight
|
||||
case .videoMessage:
|
||||
case .videoMessage, .voiceMessage:
|
||||
fullTrackHeight = 33.0
|
||||
}
|
||||
let scrubberSize = CGSize(width: availableSize.width, height: fullTrackHeight)
|
||||
|
|
@ -1207,7 +1208,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||
fullTrackHeight = trackHeight
|
||||
framesCornerRadius = 9.0
|
||||
self.videoTransparentFramesContainer.alpha = 0.35
|
||||
case .videoMessage:
|
||||
case .videoMessage, .voiceMessage:
|
||||
fullTrackHeight = 33.0
|
||||
framesCornerRadius = fullTrackHeight / 2.0
|
||||
self.videoTransparentFramesContainer.alpha = 0.5
|
||||
|
|
@ -1620,7 +1621,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||
}
|
||||
|
||||
|
||||
private class TrimView: UIView {
|
||||
public class TrimView: UIView {
|
||||
fileprivate let leftHandleView = HandleView()
|
||||
fileprivate let rightHandleView = HandleView()
|
||||
private let borderView = UIImageView()
|
||||
|
|
@ -1631,12 +1632,12 @@ private class TrimView: UIView {
|
|||
|
||||
fileprivate var isPanningTrimHandle = false
|
||||
|
||||
var isHollow = false
|
||||
public var isHollow = false
|
||||
|
||||
var trimUpdated: (Double, Double, Bool, Bool) -> Void = { _, _, _, _ in }
|
||||
public var trimUpdated: (Double, Double, Bool, Bool) -> Void = { _, _, _, _ in }
|
||||
var updated: (ComponentTransition) -> Void = { _ in }
|
||||
|
||||
override init(frame: CGRect) {
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.zoneView.image = UIImage()
|
||||
|
|
@ -1792,7 +1793,7 @@ private class TrimView: UIView {
|
|||
self.updated(transition)
|
||||
}
|
||||
|
||||
var params: (
|
||||
private var params: (
|
||||
scrubberSize: CGSize,
|
||||
duration: Double,
|
||||
startPosition: Double,
|
||||
|
|
@ -1802,7 +1803,7 @@ private class TrimView: UIView {
|
|||
maxDuration: Double
|
||||
)?
|
||||
|
||||
func update(
|
||||
public func update(
|
||||
style: MediaScrubberComponent.Style,
|
||||
theme: PresentationTheme,
|
||||
visualInsets: UIEdgeInsets,
|
||||
|
|
@ -1823,6 +1824,7 @@ private class TrimView: UIView {
|
|||
let capsuleOffset: CGFloat
|
||||
let color: UIColor
|
||||
let highlightColor: UIColor
|
||||
var borderColor: UIColor
|
||||
|
||||
switch style {
|
||||
case .editor, .cover:
|
||||
|
|
@ -1880,15 +1882,41 @@ private class TrimView: UIView {
|
|||
self.leftHandleView.image = handleImage
|
||||
self.rightHandleView.image = handleImage
|
||||
|
||||
self.leftCapsuleView.backgroundColor = .white
|
||||
self.rightCapsuleView.backgroundColor = .white
|
||||
}
|
||||
|
||||
case .voiceMessage:
|
||||
effectiveHandleWidth = 16.0
|
||||
fullTrackHeight = 33.0
|
||||
capsuleOffset = 8.0
|
||||
color = .clear
|
||||
highlightColor = .clear
|
||||
|
||||
self.zoneView.backgroundColor = UIColor(white: 1.0, alpha: 0.4)
|
||||
|
||||
if isFirstTime {
|
||||
self.borderView.image = generateImage(CGSize(width: 3.0, height: fullTrackHeight), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: CGSize(width: 1.0, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(x: size.width - 1.0, y: 0.0), size: CGSize(width: 1.0, height: size.height)))
|
||||
})?.withRenderingMode(.alwaysTemplate).resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 1.0, bottom: 0.0, right: 1.0))
|
||||
|
||||
self.leftCapsuleView.backgroundColor = .white
|
||||
self.rightCapsuleView.backgroundColor = .white
|
||||
}
|
||||
}
|
||||
|
||||
let trimColor = self.isPanningTrimHandle ? highlightColor : color
|
||||
borderColor = trimColor
|
||||
if case .voiceMessage = style {
|
||||
borderColor = theme.chat.inputPanel.panelBackgroundColor
|
||||
}
|
||||
|
||||
transition.setTintColor(view: self.leftHandleView, color: trimColor)
|
||||
transition.setTintColor(view: self.rightHandleView, color: trimColor)
|
||||
transition.setTintColor(view: self.borderView, color: trimColor)
|
||||
transition.setTintColor(view: self.borderView, color: borderColor)
|
||||
|
||||
let totalWidth = scrubberSize.width
|
||||
let totalRange = totalWidth - effectiveHandleWidth
|
||||
|
|
@ -1919,7 +1947,7 @@ private class TrimView: UIView {
|
|||
return (leftHandleFrame, rightHandleFrame)
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
let leftHandleFrame = self.leftHandleView.frame.insetBy(dx: -8.0, dy: -9.0)
|
||||
let rightHandleFrame = self.rightHandleView.frame.insetBy(dx: -8.0, dy: -9.0)
|
||||
let areaFrame = CGRect(x: leftHandleFrame.minX, y: leftHandleFrame.minY, width: rightHandleFrame.maxX - leftHandleFrame.minX, height: rightHandleFrame.maxY - rightHandleFrame.minY)
|
||||
|
|
|
|||
|
|
@ -1842,13 +1842,12 @@ public final class MessageInputPanelComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
var lightFieldColor = UIColor(white: 1.0, alpha: 0.09)
|
||||
let lightFieldColor = UIColor(white: 1.0, alpha: 0.09)
|
||||
var fieldBackgroundIsDark = false
|
||||
if component.useGrayBackground {
|
||||
fieldBackgroundIsDark = false
|
||||
} else if component.style == .media {
|
||||
fieldBackgroundIsDark = false
|
||||
lightFieldColor = UIColor(white: 0.2, alpha: 0.45)
|
||||
} else if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
||||
fieldBackgroundIsDark = true
|
||||
} else if isEditing || component.style == .story || component.style == .editor {
|
||||
|
|
|
|||
|
|
@ -600,7 +600,7 @@ private final class PeerInfoInteraction {
|
|||
let openQrCode: () -> Void
|
||||
let editingOpenReactionsSetup: () -> Void
|
||||
let dismissInput: () -> Void
|
||||
let toggleForumTopics: (Bool) -> Void
|
||||
let openForumSettings: () -> Void
|
||||
let displayTopicsLimited: (TopicsLimitedReason) -> Void
|
||||
let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void
|
||||
let openBotApp: (AttachMenuBot) -> Void
|
||||
|
|
@ -674,7 +674,7 @@ private final class PeerInfoInteraction {
|
|||
openQrCode: @escaping () -> Void,
|
||||
editingOpenReactionsSetup: @escaping () -> Void,
|
||||
dismissInput: @escaping () -> Void,
|
||||
toggleForumTopics: @escaping (Bool) -> Void,
|
||||
openForumSettings: @escaping () -> Void,
|
||||
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
|
||||
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
|
||||
openBotApp: @escaping (AttachMenuBot) -> Void,
|
||||
|
|
@ -747,7 +747,7 @@ private final class PeerInfoInteraction {
|
|||
self.openQrCode = openQrCode
|
||||
self.editingOpenReactionsSetup = editingOpenReactionsSetup
|
||||
self.dismissInput = dismissInput
|
||||
self.toggleForumTopics = toggleForumTopics
|
||||
self.openForumSettings = openForumSettings
|
||||
self.displayTopicsLimited = displayTopicsLimited
|
||||
self.openPeerMention = openPeerMention
|
||||
self.openBotApp = openBotApp
|
||||
|
|
@ -2581,11 +2581,13 @@ private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostSt
|
|||
}
|
||||
|
||||
if canSetupTopics {
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenSwitchItem(id: ItemTopics, text: presentationData.strings.PeerInfo_OptionTopics, value: channel.flags.contains(.isForum), icon: UIImage(bundleImageName: "Settings/Menu/Topics"), isLocked: topicsLimitedReason != nil, toggled: { value in
|
||||
//TODO:localize
|
||||
let label = channel.flags.contains(.isForum) ? "Enabled" : "Disabled"
|
||||
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemTopics, label: .text(label), text: presentationData.strings.PeerInfo_OptionTopics, icon: UIImage(bundleImageName: "Settings/Menu/Topics"), action: {
|
||||
if let topicsLimitedReason = topicsLimitedReason {
|
||||
interaction.displayTopicsLimited(topicsLimitedReason)
|
||||
} else {
|
||||
interaction.toggleForumTopics(value)
|
||||
interaction.openForumSettings()
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -2708,11 +2710,12 @@ private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostSt
|
|||
}
|
||||
|
||||
if canSetupTopics {
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenSwitchItem(id: ItemTopics, text: presentationData.strings.PeerInfo_OptionTopics, value: false, icon: UIImage(bundleImageName: "Settings/Menu/Topics"), isLocked: topicsLimitedReason != nil, toggled: { value in
|
||||
//TODO:localize
|
||||
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemTopics, label: .text("Disabled"), text: presentationData.strings.PeerInfo_OptionTopics, icon: UIImage(bundleImageName: "Settings/Menu/Topics"), action: {
|
||||
if let topicsLimitedReason = topicsLimitedReason {
|
||||
interaction.displayTopicsLimited(topicsLimitedReason)
|
||||
} else {
|
||||
interaction.toggleForumTopics(value)
|
||||
interaction.openForumSettings()
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -3134,11 +3137,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
dismissInput: { [weak self] in
|
||||
self?.view.endEditing(true)
|
||||
},
|
||||
toggleForumTopics: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.toggleForumTopics(isEnabled: value)
|
||||
openForumSettings: { [weak self] in
|
||||
self?.openForumSettings()
|
||||
},
|
||||
displayTopicsLimited: { [weak self] reason in
|
||||
guard let self else {
|
||||
|
|
@ -9235,6 +9235,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
})
|
||||
}
|
||||
|
||||
private func openForumSettings() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let settingsController = self.context.sharedContext.makeForumSettingsScreen(context: self.context, peerId: self.peerId)
|
||||
controller.push(settingsController)
|
||||
}
|
||||
|
||||
private func toggleForumTopics(isEnabled: Bool) {
|
||||
guard let data = self.data, let peer = data.peer else {
|
||||
return
|
||||
|
|
@ -10071,7 +10079,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
if (self.isSettings || self.isMyProfile) && self.data?.globalSettings?.privacySettings?.phoneDiscoveryEnabled == false && (self.data?.peer?.addressName ?? "").isEmpty {
|
||||
temporary = true
|
||||
}
|
||||
controller.present(self.context.sharedContext.makeChatQrCodeScreen(context: self.context, peer: peer, threadId: threadId, temporary: temporary), in: .window(.root))
|
||||
let qrController = self.context.sharedContext.makeChatQrCodeScreen(context: self.context, peer: peer, threadId: threadId, temporary: temporary)
|
||||
controller.push(qrController)
|
||||
}
|
||||
|
||||
private func openPremiumGift() {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ extension PeerInfoScreenImpl {
|
|||
if let asset = result as? PHAsset {
|
||||
subject = .single(.asset(asset))
|
||||
} else if let image = result as? UIImage {
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|
||||
} else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
|
||||
subject = result
|
||||
|> map { value -> MediaEditorScreenImpl.Subject? in
|
||||
|
|
@ -120,9 +120,9 @@ extension PeerInfoScreenImpl {
|
|||
case .pendingImage:
|
||||
return nil
|
||||
case let .image(image):
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft)
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false)
|
||||
case let .video(video):
|
||||
return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft)
|
||||
return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft, fromCamera: false)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "FetchAudioMediaResource",
|
||||
module_name = "FetchAudioMediaResource",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Postbox",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/LegacyComponents",
|
||||
"//submodules/FFMpegBinding",
|
||||
"//submodules/LocalMediaResources",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import FFMpegBinding
|
||||
import LocalMediaResources
|
||||
|
||||
public func fetchLocalFileAudioMediaResource(postbox: Postbox, resource: LocalFileAudioMediaResource) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
|
||||
let tempFile = EngineTempBox.shared.tempFile(fileName: "audio.ogg")
|
||||
FFMpegOpusTrimmer.trim(resource.path, to: tempFile.path, start: resource.trimRange?.lowerBound ?? 0.0, end: resource.trimRange?.upperBound ?? 1.0)
|
||||
|
||||
return .single(.moveTempFile(file: tempFile))
|
||||
}
|
||||
|
|
@ -189,6 +189,8 @@ public class ShareRootControllerImpl {
|
|||
|
||||
private weak var navigationController: NavigationController?
|
||||
|
||||
public var openUrl: (String) -> Void = { _ in }
|
||||
|
||||
public init(initializationData: ShareRootControllerInitializationData, getExtensionContext: @escaping () -> NSExtensionContext?) {
|
||||
self.initializationData = initializationData
|
||||
self.getExtensionContext = getExtensionContext
|
||||
|
|
@ -237,7 +239,8 @@ public class ShareRootControllerImpl {
|
|||
|
||||
setupSharedLogger(rootPath: rootPath, path: logsPath)
|
||||
|
||||
let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, appBuildType: self.initializationData.appBuildType, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { _ in
|
||||
let applicationBindings = TelegramApplicationBindings(isMainApp: false, appBundleId: self.initializationData.appBundleId, appBuildType: self.initializationData.appBuildType, containerPath: self.initializationData.appGroupPath, appSpecificScheme: "tg", openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
}, openUniversalUrl: { _, completion in
|
||||
completion.completion(false)
|
||||
return
|
||||
|
|
@ -593,6 +596,61 @@ public class ShareRootControllerImpl {
|
|||
//inForeground.set(false)
|
||||
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
}
|
||||
shareController.shareStory = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let inputItems = self.getExtensionContext()?.inputItems, inputItems.count == 1, let item = inputItems[0] as? NSExtensionItem, let attachments = item.attachments {
|
||||
let sessionId = Int64.random(in: 1000000 ..< .max)
|
||||
|
||||
let storiesPath = rootPath + "/share/stories/\(sessionId)"
|
||||
let _ = try? FileManager.default.createDirectory(atPath: storiesPath, withIntermediateDirectories: true, attributes: nil)
|
||||
var index = 0
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for attachment in attachments {
|
||||
let fileIndex = index
|
||||
if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
dispatchGroup.enter()
|
||||
attachment.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { url, _ in
|
||||
if let url, let imageData = try? Data(contentsOf: url) {
|
||||
let filePath = storiesPath + "/\(fileIndex).jpg"
|
||||
try? FileManager.default.removeItem(atPath: filePath)
|
||||
|
||||
do {
|
||||
try imageData.write(to: URL(fileURLWithPath: filePath))
|
||||
} catch {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
})
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
|
||||
dispatchGroup.enter()
|
||||
attachment.loadFileRepresentation(forTypeIdentifier: kUTTypeMovie as String, completionHandler: { url, _ in
|
||||
if let url {
|
||||
let filePath = storiesPath + "/\(fileIndex).mp4"
|
||||
try? FileManager.default.removeItem(atPath: filePath)
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: url, to: URL(fileURLWithPath: filePath))
|
||||
} catch {
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
})
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
self.openUrl("tg://shareStory?session=\(sessionId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
/*shareController.debugAction = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -926,7 +926,7 @@ final class StoryItemSetContainerSendMessage {
|
|||
}
|
||||
} else {
|
||||
if self.audioRecorderValue == nil {
|
||||
self.audioRecorder.set(component.context.sharedContext.mediaManager.audioRecorder(beginWithTone: false, applicationBindings: component.context.sharedContext.applicationBindings, beganWithTone: { _ in
|
||||
self.audioRecorder.set(component.context.sharedContext.mediaManager.audioRecorder(resumeData: nil, beginWithTone: false, applicationBindings: component.context.sharedContext.applicationBindings, beganWithTone: { _ in
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ swift_library(
|
|||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ICloudResources",
|
||||
"//submodules/TelegramUI/Components/Resources/FetchVideoMediaResource",
|
||||
"//submodules/TelegramUI/Components/Resources/FetchAudioMediaResource",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import AppBundle
|
|||
import SwiftSignalKit
|
||||
import ICloudResources
|
||||
import FetchVideoMediaResource
|
||||
import FetchAudioMediaResource
|
||||
import Display
|
||||
|
||||
public func makeTelegramAccountAuxiliaryMethods(uploadInBackground: ((Postbox, MediaResource) -> Signal<String?, NoError>)?) -> AccountAuxiliaryMethods {
|
||||
|
|
@ -43,6 +44,8 @@ public func makeTelegramAccountAuxiliaryMethods(uploadInBackground: ((Postbox, M
|
|||
|> mapToSignal { useModernPipeline -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||
return fetchLocalFileVideoMediaResource(postbox: postbox, resource: resource, alwaysUseModernPipeline: useModernPipeline)
|
||||
}
|
||||
} else if let resource = resource as? LocalFileAudioMediaResource {
|
||||
return fetchLocalFileAudioMediaResource(postbox: postbox, resource: resource)
|
||||
} else if let resource = resource as? LocalFileGifMediaResource {
|
||||
return fetchLocalFileGifMediaResource(resource: resource)
|
||||
} else if let photoLibraryResource = resource as? PhotoLibraryMediaResource {
|
||||
|
|
@ -56,7 +59,7 @@ public func makeTelegramAccountAuxiliaryMethods(uploadInBackground: ((Postbox, M
|
|||
}
|
||||
|> castError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { useExif -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||
return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier, width: photoLibraryResource.width, height: photoLibraryResource.height, format: photoLibraryResource.format, quality: photoLibraryResource.quality, useExif: useExif)
|
||||
return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier, width: photoLibraryResource.width, height: photoLibraryResource.height, format: photoLibraryResource.format, quality: photoLibraryResource.quality, hd: photoLibraryResource.forceHd, useExif: useExif)
|
||||
}
|
||||
} else if let resource = resource as? ICloudFileResource {
|
||||
return fetchICloudFileResource(resource: resource)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import ICloudResources
|
|||
private var telegramUIDeclaredEncodables: Void = {
|
||||
declareEncodable(VideoLibraryMediaResource.self, f: { VideoLibraryMediaResource(decoder: $0) })
|
||||
declareEncodable(LocalFileVideoMediaResource.self, f: { LocalFileVideoMediaResource(decoder: $0) })
|
||||
declareEncodable(LocalFileAudioMediaResource.self, f: { LocalFileAudioMediaResource(decoder: $0) })
|
||||
declareEncodable(LocalFileGifMediaResource.self, f: { LocalFileGifMediaResource(decoder: $0) })
|
||||
declareEncodable(PhotoLibraryMediaResource.self, f: { PhotoLibraryMediaResource(decoder: $0) })
|
||||
declareEncodable(ICloudFileResource.self, f: { ICloudFileResource(decoder: $0) })
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Settings/ScanQr.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/ScanQr.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "scanqr_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Settings/ScanQr.imageset/scanqr_24.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Settings/ScanQr.imageset/scanqr_24.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Share/Story.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Share/Story.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "addtostory.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Share/Story.imageset/addtostory.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Share/Story.imageset/addtostory.pdf
vendored
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/ForumList.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/ForumList.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/ForumTabs.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/ForumTabs.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/Topics.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/Topics.tgs
Normal file
Binary file not shown.
|
|
@ -5986,6 +5986,8 @@ extension ChatControllerImpl {
|
|||
}, updateVideoTrimRange: { [weak self] start, end, updatedEnd, apply in
|
||||
if let videoRecorder = self?.videoRecorderValue {
|
||||
videoRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
||||
} else if let audioRecorder = self?.audioRecorderValue {
|
||||
audioRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
||||
}
|
||||
}, updateHistoryFilter: { [weak self] update in
|
||||
guard let self else {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import TelegramIntents
|
|||
import TooltipUI
|
||||
import StatisticsUI
|
||||
import MediaResources
|
||||
import LocalMediaResources
|
||||
import GalleryData
|
||||
import ChatInterfaceState
|
||||
import InviteLinksUI
|
||||
|
|
@ -125,15 +126,27 @@ import ChatMediaInputStickerGridItem
|
|||
import AdsInfoScreen
|
||||
|
||||
extension ChatControllerImpl {
|
||||
func requestAudioRecorder(beginWithTone: Bool) {
|
||||
func requestAudioRecorder(beginWithTone: Bool, existingDraft: ChatInterfaceMediaDraftState.Audio? = nil) {
|
||||
if self.audioRecorderValue == nil {
|
||||
if self.recorderFeedback == nil {
|
||||
if self.recorderFeedback == nil && existingDraft == nil {
|
||||
self.recorderFeedback = HapticFeedback()
|
||||
self.recorderFeedback?.prepareImpact(.light)
|
||||
}
|
||||
|
||||
self.audioRecorder.set(self.context.sharedContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, applicationBindings: self.context.sharedContext.applicationBindings, beganWithTone: { _ in
|
||||
}))
|
||||
var resumeData: AudioRecorderResumeData?
|
||||
if let existingDraft, let path = self.context.account.postbox.mediaBox.completedResourcePath(existingDraft.resource), let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let recorderResumeData = existingDraft.resumeData {
|
||||
resumeData = AudioRecorderResumeData(compressedData: compressedData, resumeData: recorderResumeData)
|
||||
}
|
||||
|
||||
self.audioRecorder.set(
|
||||
self.context.sharedContext.mediaManager.audioRecorder(
|
||||
resumeData: resumeData,
|
||||
beginWithTone: beginWithTone,
|
||||
applicationBindings: self.context.sharedContext.applicationBindings,
|
||||
beganWithTone: { _ in
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,46 +282,59 @@ extension ChatControllerImpl {
|
|||
self.chatDisplayNode.updateRecordedMediaDeleted(true)
|
||||
self.audioRecorder.set(.single(nil))
|
||||
case .preview, .pause:
|
||||
if case .preview = updatedAction {
|
||||
self.audioRecorder.set(.single(nil))
|
||||
}
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(.waitingForPreview)
|
||||
}
|
||||
})
|
||||
self.recorderDataDisposable.set((audioRecorderValue.takenRecordedData()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] data in
|
||||
if let strongSelf = self, let data = data {
|
||||
if data.duration < 0.5 {
|
||||
strongSelf.recorderFeedback?.error()
|
||||
strongSelf.recorderFeedback = nil
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(nil)
|
||||
|
||||
var resource: LocalFileMediaResource?
|
||||
self.recorderDataDisposable.set(
|
||||
(audioRecorderValue.takenRecordedData()
|
||||
|> deliverOnMainQueue).startStrict(
|
||||
next: { [weak self] data in
|
||||
if let strongSelf = self, let data = data {
|
||||
if data.duration < 0.5 {
|
||||
strongSelf.recorderFeedback?.error()
|
||||
strongSelf.recorderFeedback = nil
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(nil)
|
||||
}
|
||||
})
|
||||
strongSelf.recorderDataDisposable.set(nil)
|
||||
} else if let waveform = data.waveform {
|
||||
if resource == nil {
|
||||
resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count))
|
||||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource!.id, data: data.compressedData)
|
||||
}
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInterfaceState {
|
||||
$0.withUpdatedMediaDraftState(.audio(
|
||||
ChatInterfaceMediaDraftState.Audio(
|
||||
resource: resource!,
|
||||
fileSize: Int32(data.compressedData.count),
|
||||
duration: Int32(data.duration),
|
||||
waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5),
|
||||
trimRange: data.trimRange,
|
||||
resumeData: data.resumeData
|
||||
)
|
||||
))
|
||||
}.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(nil)
|
||||
}
|
||||
})
|
||||
strongSelf.recorderFeedback = nil
|
||||
strongSelf.updateDownButtonVisibility()
|
||||
|
||||
if sendImmediately {
|
||||
strongSelf.interfaceInteraction?.sendRecordedMedia(false, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
strongSelf.recorderDataDisposable.set(nil)
|
||||
} else if let waveform = data.waveform {
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count))
|
||||
|
||||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData)
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedMediaDraftState(.audio(ChatInterfaceMediaDraftState.Audio(resource: resource, fileSize: Int32(data.compressedData.count), duration: Int32(data.duration), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5)))) }.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(nil)
|
||||
}
|
||||
})
|
||||
strongSelf.recorderFeedback = nil
|
||||
strongSelf.updateDownButtonVisibility()
|
||||
strongSelf.recorderDataDisposable.set(nil)
|
||||
|
||||
if sendImmediately {
|
||||
strongSelf.interfaceInteraction?.sendRecordedMedia(false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
)
|
||||
case let .send(viewOnce):
|
||||
self.chatDisplayNode.updateRecordedMediaDeleted(false)
|
||||
self.recorderDataDisposable.set((audioRecorderValue.takenRecordedData()
|
||||
|
|
@ -449,7 +475,21 @@ extension ChatControllerImpl {
|
|||
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorderValue, isLocked: true))
|
||||
}.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) }
|
||||
})
|
||||
} else if let videoRecorderValue = self.videoRecorderValue {
|
||||
} else if let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview {
|
||||
self.requestAudioRecorder(beginWithTone: false, existingDraft: audio)
|
||||
|
||||
if let audioRecorderValue = self.audioRecorderValue {
|
||||
audioRecorderValue.resume()
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorderValue, isLocked: true))
|
||||
}.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let videoRecorderValue = self.videoRecorderValue {
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
$0.updatedInputTextPanelState { panelState in
|
||||
let recordingStatus = videoRecorderValue.recordingStatus
|
||||
|
|
@ -486,7 +526,13 @@ extension ChatControllerImpl {
|
|||
self.updateDownButtonVisibility()
|
||||
}
|
||||
|
||||
func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false, messageEffect: ChatSendMessageEffect? = nil, postpone: Bool = false) {
|
||||
func sendMediaRecording(
|
||||
silentPosting: Bool? = nil,
|
||||
scheduleTime: Int32? = nil,
|
||||
viewOnce: Bool = false,
|
||||
messageEffect: ChatSendMessageEffect? = nil,
|
||||
postpone: Bool = false
|
||||
) {
|
||||
self.chatDisplayNode.updateRecordedMediaDeleted(false)
|
||||
|
||||
guard let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState else {
|
||||
|
|
@ -531,7 +577,20 @@ extension ChatControllerImpl {
|
|||
attributes.append(EffectMessageAttribute(id: messageEffect.id))
|
||||
}
|
||||
|
||||
let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
|
||||
let resource: TelegramMediaResource
|
||||
var finalDuration: Int = Int(audio.duration)
|
||||
if let trimRange = audio.trimRange, trimRange.lowerBound > 0.0 || trimRange.upperBound < Double(audio.duration) {
|
||||
let randomId = Int64.random(in: Int64.min ... Int64.max)
|
||||
let tempPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).ogg"
|
||||
resource = LocalFileAudioMediaResource(randomId: randomId, path: tempPath, trimRange: audio.trimRange)
|
||||
self.context.account.postbox.mediaBox.moveResourceData(audio.resource.id, toTempPath: tempPath)
|
||||
|
||||
finalDuration = Int(trimRange.upperBound - trimRange.lowerBound)
|
||||
} else {
|
||||
resource = audio.resource
|
||||
}
|
||||
|
||||
let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: finalDuration, title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
|
||||
|
||||
let transformedMessages: [EnqueueMessage]
|
||||
if let silentPosting = silentPosting {
|
||||
|
|
|
|||
|
|
@ -5386,9 +5386,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
}
|
||||
audioRecorder.start()
|
||||
strongSelf.audioRecorderStatusDisposable = (audioRecorder.recordingState
|
||||
|> deliverOnMainQueue).startStrict(next: { value in
|
||||
if case .stopped = value {
|
||||
self?.stopMediaRecorder()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
||||
if let self, case .stopped = value {
|
||||
if self.presentationInterfaceState.interfaceState.mediaDraftState != nil {
|
||||
|
||||
} else {
|
||||
self.stopMediaRecorder()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1893,7 +1893,7 @@ extension ChatControllerImpl {
|
|||
if let asset = result as? PHAsset {
|
||||
subject = .single(.asset(asset))
|
||||
} else if let image = result as? UIImage {
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|
||||
} else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
|
||||
subject = result
|
||||
|> map { value -> MediaEditorScreenImpl.Subject? in
|
||||
|
|
@ -1901,7 +1901,7 @@ extension ChatControllerImpl {
|
|||
case .pendingImage:
|
||||
return nil
|
||||
case let .image(image):
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft)
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ extension ChatControllerImpl {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.present(ChatQrCodeScreen(context: self.context, subject: .messages(messages)), in: .window(.root))
|
||||
self.present(ChatQrCodeScreenImpl(context: self.context, subject: .messages(messages)), in: .window(.root))
|
||||
}
|
||||
shareController.dismissed = { [weak self] shared in
|
||||
if shared {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,68 @@ final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelVie
|
|||
}
|
||||
}
|
||||
|
||||
final class PlayButtonNode: ASDisplayNode {
|
||||
let backgroundNode: ASDisplayNode
|
||||
let playButton: HighlightableButtonNode
|
||||
fileprivate let playPauseIconNode: PlayPauseIconNode
|
||||
let durationLabel: MediaPlayerTimeTextNode
|
||||
|
||||
var pressed: () -> Void = {}
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
|
||||
self.backgroundNode.cornerRadius = 11.0
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.playButton = HighlightableButtonNode()
|
||||
self.playButton.displaysAsynchronously = false
|
||||
|
||||
self.playPauseIconNode = PlayPauseIconNode()
|
||||
self.playPauseIconNode.enqueueState(.play, animated: false)
|
||||
self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor
|
||||
|
||||
self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.chat.inputPanel.actionControlForegroundColor, textFont: Font.with(size: 13.0, weight: .semibold, traits: .monospacedNumbers))
|
||||
self.durationLabel.alignment = .right
|
||||
self.durationLabel.mode = .normal
|
||||
self.durationLabel.showDurationIfNotStarted = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.playButton)
|
||||
self.backgroundNode.addSubnode(self.playPauseIconNode)
|
||||
self.backgroundNode.addSubnode(self.durationLabel)
|
||||
|
||||
self.playButton.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.backgroundNode.frame.contains(point)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
var buttonSize = CGSize(width: 63.0, height: 22.0)
|
||||
if size.width < 70.0 {
|
||||
buttonSize.width = 27.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: buttonSize.centered(in: CGRect(origin: .zero, size: size)))
|
||||
|
||||
self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 1.0 - UIScreenPixel), size: CGSize(width: 21.0, height: 21.0))
|
||||
|
||||
transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: 18.0, y: 3.0), size: CGSize(width: 35.0, height: 20.0)))
|
||||
transition.updateAlpha(node: self.durationLabel, alpha: buttonSize.width > 27.0 ? 1.0 : 0.0)
|
||||
|
||||
self.playButton.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
let deleteButton: HighlightableButtonNode
|
||||
let binNode: AnimationNode
|
||||
|
|
@ -75,11 +137,12 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
let textNode: ImmediateAnimatedCountLabelNode
|
||||
|
||||
private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
|
||||
let playButton: HighlightableButtonNode
|
||||
private let playPauseIconNode: PlayPauseIconNode
|
||||
private let waveformButton: ASButtonNode
|
||||
let waveformBackgroundNode: ASImageNode
|
||||
|
||||
let trimView: TrimView
|
||||
let playButtonNode: PlayButtonNode
|
||||
|
||||
let scrubber = ComponentView<Empty>()
|
||||
|
||||
var viewOnce = false
|
||||
|
|
@ -93,7 +156,6 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
private var presentationInterfaceState: ChatPresentationInterfaceState?
|
||||
|
||||
private var mediaPlayer: MediaPlayer?
|
||||
let durationLabel: MediaPlayerTimeTextNode
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
|
||||
|
|
@ -139,13 +201,6 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
self.waveformBackgroundNode.displayWithoutProcessing = true
|
||||
self.waveformBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputPanel.actionControlFillColor)
|
||||
|
||||
self.playButton = HighlightableButtonNode()
|
||||
self.playButton.displaysAsynchronously = false
|
||||
|
||||
self.playPauseIconNode = PlayPauseIconNode()
|
||||
self.playPauseIconNode.enqueueState(.play, animated: false)
|
||||
self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor
|
||||
|
||||
self.waveformButton = ASButtonNode()
|
||||
self.waveformButton.accessibilityTraits.insert(.startsMediaSession)
|
||||
|
||||
|
|
@ -156,9 +211,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
|
||||
self.waveformScrubberNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: self.waveformNode, foregroundContentNode: self.waveformForegroundNode))
|
||||
|
||||
self.durationLabel = MediaPlayerTimeTextNode(textColor: theme.chat.inputPanel.actionControlForegroundColor)
|
||||
self.durationLabel.alignment = .right
|
||||
self.durationLabel.mode = .normal
|
||||
self.trimView = TrimView(frame: .zero)
|
||||
self.trimView.isHollow = true
|
||||
self.playButtonNode = PlayButtonNode(theme: theme)
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -185,10 +240,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
self.sendButton.addSubnode(self.sendIconNode)
|
||||
self.sendButton.addSubnode(self.textNode)
|
||||
self.addSubnode(self.waveformScrubberNode)
|
||||
self.addSubnode(self.playButton)
|
||||
self.addSubnode(self.durationLabel)
|
||||
self.addSubnode(self.waveformButton)
|
||||
self.playButton.addSubnode(self.playPauseIconNode)
|
||||
//self.addSubnode(self.waveformButton)
|
||||
|
||||
//self.view.addSubview(self.trimView)
|
||||
self.addSubnode(self.playButtonNode)
|
||||
|
||||
self.sendButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
|
|
@ -199,7 +254,21 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.playButtonNode.pressed = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.waveformPressed()
|
||||
}
|
||||
|
||||
self.waveformScrubberNode.seek = { [weak self] timestamp in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.mediaPlayer?.seek(timestamp: timestamp)
|
||||
}
|
||||
|
||||
self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside])
|
||||
self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside])
|
||||
self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside])
|
||||
|
|
@ -315,6 +384,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
transition.updateFrame(node: self.sendIconNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - icon.size.width) / 2.0), y: floorToScreenPixels((innerSize.height - icon.size.height) / 2.0)), size: icon.size))
|
||||
}
|
||||
|
||||
let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: 33.0))
|
||||
|
||||
if self.presentationInterfaceState != interfaceState {
|
||||
var updateWaveform = false
|
||||
if self.presentationInterfaceState?.interfaceState.mediaDraftState != interfaceState.interfaceState.mediaDraftState {
|
||||
|
|
@ -335,8 +406,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
self.waveformBackgroundNode.isHidden = false
|
||||
self.waveformForegroundNode.isHidden = false
|
||||
self.waveformScrubberNode.isHidden = false
|
||||
self.playButton.isHidden = false
|
||||
self.durationLabel.isHidden = false
|
||||
self.playButtonNode.isHidden = false
|
||||
|
||||
if let view = self.scrubber.view, view.superview != nil {
|
||||
view.removeFromSuperview()
|
||||
|
|
@ -350,32 +420,71 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
}
|
||||
let mediaManager = context.sharedContext.mediaManager
|
||||
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: .standalone(resource: audio.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true)
|
||||
mediaPlayer.actionAtEnd = .action { [weak mediaPlayer] in
|
||||
mediaPlayer?.seek(timestamp: 0.0)
|
||||
mediaPlayer.actionAtEnd = .action { [weak self] in
|
||||
guard let self, let interfaceState = self.presentationInterfaceState else {
|
||||
return
|
||||
}
|
||||
var timestamp: Double = 0.0
|
||||
if let recordedMediaPreview = interfaceState.interfaceState.mediaDraftState, case let .audio(audio) = recordedMediaPreview, let trimRange = audio.trimRange {
|
||||
timestamp = trimRange.lowerBound
|
||||
}
|
||||
self.mediaPlayer?.seek(timestamp: timestamp)
|
||||
}
|
||||
self.mediaPlayer = mediaPlayer
|
||||
self.durationLabel.defaultDuration = Double(audio.duration)
|
||||
self.durationLabel.status = mediaPlayer.status
|
||||
self.playButtonNode.durationLabel.defaultDuration = Double(audio.duration)
|
||||
self.playButtonNode.durationLabel.status = mediaPlayer.status
|
||||
self.playButtonNode.durationLabel.trimRange = audio.trimRange
|
||||
self.waveformScrubberNode.status = mediaPlayer.status
|
||||
self.statusDisposable.set((mediaPlayer.status
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
switch status.status {
|
||||
case .playing, .buffering(_, true, _, _):
|
||||
strongSelf.playPauseIconNode.enqueueState(.pause, animated: true)
|
||||
strongSelf.playButtonNode.playPauseIconNode.enqueueState(.pause, animated: true)
|
||||
default:
|
||||
strongSelf.playPauseIconNode.enqueueState(.play, animated: true)
|
||||
strongSelf.playButtonNode.playPauseIconNode.enqueueState(.play, animated: true)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
let (leftHandleFrame, rightHandleFrame) = self.trimView.update(
|
||||
style: .voiceMessage,
|
||||
theme: interfaceState.theme,
|
||||
visualInsets: .zero,
|
||||
scrubberSize: waveformBackgroundFrame.size,
|
||||
duration: Double(audio.duration),
|
||||
startPosition: audio.trimRange?.lowerBound ?? 0.0,
|
||||
endPosition: audio.trimRange?.upperBound ?? Double(audio.duration),
|
||||
position: 0.0,
|
||||
minDuration: 2.0,
|
||||
maxDuration: Double(audio.duration),
|
||||
transition: .immediate
|
||||
)
|
||||
self.trimView.trimUpdated = { [weak self] start, end, updatedEnd, apply in
|
||||
if let self {
|
||||
self.mediaPlayer?.pause()
|
||||
self.interfaceInteraction?.updateVideoTrimRange(start, end, updatedEnd, apply)
|
||||
if apply {
|
||||
if !updatedEnd {
|
||||
self.mediaPlayer?.seek(timestamp: start, play: true)
|
||||
} else {
|
||||
self.mediaPlayer?.seek(timestamp: end - 1.0, play: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.trimView.frame = waveformBackgroundFrame
|
||||
|
||||
let playButtonSize = CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX, height: waveformBackgroundFrame.height)
|
||||
self.playButtonNode.update(size: playButtonSize, transition: transition)
|
||||
transition.updateFrame(node: self.playButtonNode, frame: CGRect(origin: CGPoint(x: waveformBackgroundFrame.minX + leftHandleFrame.maxX, y: waveformBackgroundFrame.minY), size: playButtonSize))
|
||||
case let .video(video):
|
||||
self.waveformButton.isHidden = true
|
||||
self.waveformBackgroundNode.isHidden = true
|
||||
self.waveformForegroundNode.isHidden = true
|
||||
self.waveformScrubberNode.isHidden = true
|
||||
self.playButton.isHidden = true
|
||||
self.durationLabel.isHidden = true
|
||||
self.playButtonNode.isHidden = true
|
||||
|
||||
let scrubberSize = self.scrubber.update(
|
||||
transition: .immediate,
|
||||
|
|
@ -412,7 +521,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
),
|
||||
environment: {},
|
||||
forceUpdate: false,
|
||||
containerSize: CGSize(width: min(424, width - leftInset - rightInset - 45.0 - innerSize.width - 1.0), height: 33.0)
|
||||
containerSize: CGSize(width: min(424.0, width - leftInset - rightInset - 45.0 - innerSize.width - 1.0), height: 33.0)
|
||||
)
|
||||
|
||||
if let view = self.scrubber.view {
|
||||
|
|
@ -475,14 +584,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
transition.updateSublayerTransformScale(layer: self.sendButton.layer, scale: CGPoint(x: 1.0, y: 1.0))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0)))
|
||||
self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: -2.0, y: -1.0), size: CGSize(width: 26.0, height: 26.0))
|
||||
|
||||
let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: 33.0))
|
||||
transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame)
|
||||
transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0, height: panelHeight)))
|
||||
transition.updateFrame(node: self.waveformScrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 35.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 1.0 - 45.0 - 40.0, height: 13.0)))
|
||||
transition.updateFrame(node: self.durationLabel, frame: CGRect(origin: CGPoint(x: width - rightInset - 45.0 - innerSize.width - 1.0 - 4.0, y: 15.0), size: CGSize(width: 35.0, height: 20.0)))
|
||||
transition.updateFrame(node: self.waveformScrubberNode, frame: CGRect(origin: CGPoint(x: leftInset + 45.0 + 21.0, y: 7.0 + floor((33.0 - 13.0) / 2.0)), size: CGSize(width: width - leftInset - rightInset - 45.0 - innerSize.width - 41.0, height: 13.0)))
|
||||
|
||||
prevInputPanelNode?.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight))
|
||||
if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode {
|
||||
|
|
@ -536,10 +640,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
|||
self.deleteButton.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15)
|
||||
self.deleteButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
self.playButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1)
|
||||
self.playButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
|
||||
|
||||
self.durationLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.1)
|
||||
self.playButtonNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1)
|
||||
self.playButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
|
||||
|
||||
self.trimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
|
||||
|
||||
self.waveformScrubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1)
|
||||
self.waveformScrubberNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
|
||||
|
|
@ -664,7 +768,7 @@ private final class PlayPauseIconNode: ManagedAnimationNode {
|
|||
private var iconState: PlayPauseIconNodeState = .pause
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 28.0, height: 28.0))
|
||||
super.init(size: CGSize(width: 21.0, height: 21.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,7 +590,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
|
||||
var emojiStatus: PeerEmojiStatus?
|
||||
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, let emojiStatusValue = user.emojiStatus {
|
||||
if user.isFake || user.isScam {
|
||||
if user.isFake || user.isScam {
|
||||
} else {
|
||||
emojiStatus = emojiStatusValue
|
||||
}
|
||||
|
|
@ -601,7 +601,8 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||
}
|
||||
}
|
||||
|
||||
if let emojiStatus = emojiStatus, case let .emoji(fileId) = emojiStatus.content {
|
||||
if let emojiStatus {
|
||||
let fileId = emojiStatus.fileId
|
||||
if self.emojiStatusFileId != fileId {
|
||||
self.emojiStatusFileId = fileId
|
||||
|
||||
|
|
|
|||
|
|
@ -2804,8 +2804,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
|||
|
||||
animatePosition(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer)
|
||||
animatePosition(for: prevPreviewInputPanelNode.waveformScrubberNode.layer)
|
||||
animatePosition(for: prevPreviewInputPanelNode.durationLabel.layer)
|
||||
animatePosition(for: prevPreviewInputPanelNode.playButton.layer)
|
||||
animatePosition(for: prevPreviewInputPanelNode.playButtonNode.layer)
|
||||
animatePosition(for: prevPreviewInputPanelNode.trimView.layer)
|
||||
if let view = prevPreviewInputPanelNode.scrubber.view {
|
||||
animatePosition(for: view.layer)
|
||||
}
|
||||
|
|
@ -2821,8 +2821,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
|||
}
|
||||
animateAlpha(for: prevPreviewInputPanelNode.waveformBackgroundNode.layer)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.waveformScrubberNode.layer)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.durationLabel.layer)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.playButton.layer)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.playButtonNode.layer)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.trimView.layer)
|
||||
if let view = prevPreviewInputPanelNode.scrubber.view {
|
||||
animateAlpha(for: view.layer)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,8 @@ final class ManagedAudioRecorderContext {
|
|||
private let beginWithTone: Bool
|
||||
private let beganWithTone: (Bool) -> Void
|
||||
|
||||
private var trimRange: Range<Double>?
|
||||
|
||||
private var paused = true
|
||||
|
||||
private let queue: Queue
|
||||
|
|
@ -184,7 +186,16 @@ final class ManagedAudioRecorderContext {
|
|||
private var toneTimer: SwiftSignalKit.Timer?
|
||||
private var idleTimerExtensionDisposable: Disposable?
|
||||
|
||||
init(queue: Queue, mediaManager: MediaManager, pushIdleTimerExtension: @escaping () -> Disposable, micLevel: ValuePromise<Float>, recordingState: ValuePromise<AudioRecordingState>, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) {
|
||||
init(
|
||||
queue: Queue,
|
||||
mediaManager: MediaManager,
|
||||
resumeData: AudioRecorderResumeData?,
|
||||
pushIdleTimerExtension: @escaping () -> Disposable,
|
||||
micLevel: ValuePromise<Float>,
|
||||
recordingState: ValuePromise<AudioRecordingState>,
|
||||
beginWithTone: Bool,
|
||||
beganWithTone: @escaping (Bool) -> Void
|
||||
) {
|
||||
assert(queue.isCurrent())
|
||||
|
||||
self.id = getNextRecorderContextId()
|
||||
|
|
@ -196,9 +207,14 @@ final class ManagedAudioRecorderContext {
|
|||
|
||||
self.queue = queue
|
||||
self.mediaManager = mediaManager
|
||||
self.dataItem = TGDataItem()
|
||||
self.oggWriter = TGOggOpusWriter()
|
||||
|
||||
if let resumeData {
|
||||
self.dataItem = TGDataItem(data: resumeData.compressedData)
|
||||
} else {
|
||||
self.dataItem = TGDataItem()
|
||||
}
|
||||
|
||||
if beginWithTone, let toneData = audioRecordingToneData {
|
||||
self.processSamples = false
|
||||
let toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ [weak self] control in
|
||||
|
|
@ -313,7 +329,19 @@ final class ManagedAudioRecorderContext {
|
|||
addAudioRecorderContext(self.id, self)
|
||||
addAudioUnitHolder(self.id, queue, self.audioUnit)
|
||||
|
||||
self.oggWriter.begin(with: self.dataItem)
|
||||
if let resumeData {
|
||||
guard let stateDict = try? JSONSerialization.jsonObject(with: resumeData.resumeData, options: []) as? [String: Any] else {
|
||||
Logger.shared.log("ManagedAudioRecorder", "Failed to deserialize JSON")
|
||||
return
|
||||
}
|
||||
let success = self.oggWriter.resume(with: self.dataItem, encoderState: stateDict)
|
||||
if !success {
|
||||
Logger.shared.log("ManagedAudioRecorder", "Failed to resume OggWriter")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
self.oggWriter.begin(with: self.dataItem)
|
||||
}
|
||||
|
||||
self.idleTimerExtensionDisposable = (Signal<Void, NoError> { subscriber in
|
||||
return pushIdleTimerExtension()
|
||||
|
|
@ -453,7 +481,7 @@ final class ManagedAudioRecorderContext {
|
|||
func pause() {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
self.stop()
|
||||
return self.stop()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
|
|
@ -462,9 +490,12 @@ final class ManagedAudioRecorderContext {
|
|||
self.start()
|
||||
}
|
||||
|
||||
private var resumeData: Data?
|
||||
func stop() {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
let state = self.oggWriter.pause()
|
||||
|
||||
self.paused = true
|
||||
|
||||
if let audioUnit = self.audioUnit.swap(nil) {
|
||||
|
|
@ -494,6 +525,19 @@ final class ManagedAudioRecorderContext {
|
|||
let audioSessionDisposable = self.audioSessionDisposable
|
||||
self.audioSessionDisposable = nil
|
||||
audioSessionDisposable?.dispose()
|
||||
|
||||
if let stateDict = state as? [String: Any] {
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: stateDict, options: [])
|
||||
self.resumeData = jsonData
|
||||
} catch {
|
||||
Logger.shared.log("ManagedAudioRecorder", "Failed to JSON: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTrimRange(start: Double, end: Double, updatedEnd: Bool, apply: Bool) {
|
||||
self.trimRange = start..<end
|
||||
}
|
||||
|
||||
func processAndDisposeAudioBuffer(_ buffer: AudioBuffer) {
|
||||
|
|
@ -668,18 +712,29 @@ final class ManagedAudioRecorderContext {
|
|||
waveform = AudioWaveform(bitstream: bitstream, bitsPerSample: 5).makeBitstream()
|
||||
}
|
||||
|
||||
return RecordedAudioData(compressedData: self.dataItem.data(), duration: self.oggWriter.encodedDuration(), waveform: waveform)
|
||||
return RecordedAudioData(
|
||||
compressedData: self.dataItem.data(),
|
||||
resumeData: self.resumeData,
|
||||
duration: self.oggWriter.encodedDuration(),
|
||||
waveform: waveform,
|
||||
trimRange: nil
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioPreviewState: Equatable {
|
||||
let trimRange: Range<Double>?
|
||||
}
|
||||
|
||||
final class ManagedAudioRecorderImpl: ManagedAudioRecorder {
|
||||
private let queue = Queue()
|
||||
private var contextRef: Unmanaged<ManagedAudioRecorderContext>?
|
||||
private let micLevelValue = ValuePromise<Float>(0.0)
|
||||
private let recordingStateValue = ValuePromise<AudioRecordingState>(.paused(duration: 0.0))
|
||||
private let previewStateValue = ValuePromise<AudioPreviewState>(AudioPreviewState(trimRange: nil))
|
||||
|
||||
let beginWithTone: Bool
|
||||
|
||||
|
|
@ -691,10 +746,16 @@ final class ManagedAudioRecorderImpl: ManagedAudioRecorder {
|
|||
return self.recordingStateValue.get()
|
||||
}
|
||||
|
||||
init(mediaManager: MediaManager, pushIdleTimerExtension: @escaping () -> Disposable, beginWithTone: Bool, beganWithTone: @escaping (Bool) -> Void) {
|
||||
init(
|
||||
mediaManager: MediaManager,
|
||||
resumeData: AudioRecorderResumeData?,
|
||||
pushIdleTimerExtension: @escaping () -> Disposable,
|
||||
beginWithTone: Bool,
|
||||
beganWithTone: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.beginWithTone = beginWithTone
|
||||
self.queue.async {
|
||||
let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, pushIdleTimerExtension: pushIdleTimerExtension, micLevel: self.micLevelValue, recordingState: self.recordingStateValue, beginWithTone: beginWithTone, beganWithTone: beganWithTone)
|
||||
let context = ManagedAudioRecorderContext(queue: self.queue, mediaManager: mediaManager, resumeData: resumeData, pushIdleTimerExtension: pushIdleTimerExtension, micLevel: self.micLevelValue, recordingState: self.recordingStateValue, beginWithTone: beginWithTone, beganWithTone: beganWithTone)
|
||||
self.contextRef = Unmanaged.passRetained(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -739,7 +800,7 @@ final class ManagedAudioRecorderImpl: ManagedAudioRecorder {
|
|||
}
|
||||
|
||||
func takenRecordedData() -> Signal<RecordedAudioData?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let dataState: Signal<RecordedAudioData?, NoError> = Signal { subscriber in
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
subscriber.putNext(context.takeData())
|
||||
|
|
@ -751,5 +812,32 @@ final class ManagedAudioRecorderImpl: ManagedAudioRecorder {
|
|||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
let previewState = self.previewStateValue.get()
|
||||
|
||||
return combineLatest(
|
||||
dataState,
|
||||
previewState
|
||||
) |> map { data, preview -> RecordedAudioData? in
|
||||
if let data {
|
||||
return RecordedAudioData(
|
||||
compressedData: data.compressedData,
|
||||
resumeData: data.resumeData,
|
||||
duration: data.duration,
|
||||
waveform: data.waveform,
|
||||
trimRange: preview.trimRange
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTrimRange(start: Double, end: Double, updatedEnd: Bool, apply: Bool) {
|
||||
self.previewStateValue.set(AudioPreviewState(trimRange: start ..< end))
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,12 +449,17 @@ public final class MediaManagerImpl: NSObject, MediaManager {
|
|||
self.voiceMediaPlayerStateDisposable.dispose()
|
||||
}
|
||||
|
||||
public func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal<ManagedAudioRecorder?, NoError> {
|
||||
public func audioRecorder(
|
||||
resumeData: AudioRecorderResumeData?,
|
||||
beginWithTone: Bool,
|
||||
applicationBindings: TelegramApplicationBindings,
|
||||
beganWithTone: @escaping (Bool) -> Void
|
||||
) -> Signal<ManagedAudioRecorder?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.queue.async {
|
||||
let audioRecorder = ManagedAudioRecorderImpl(mediaManager: self, pushIdleTimerExtension: { [weak applicationBindings] in
|
||||
let audioRecorder = ManagedAudioRecorderImpl(mediaManager: self, resumeData: resumeData, pushIdleTimerExtension: { [weak applicationBindings] in
|
||||
return applicationBindings?.pushIdleTimerExtension() ?? EmptyDisposable
|
||||
}, beginWithTone: beginWithTone, beganWithTone: beganWithTone)
|
||||
subscriber.putNext(audioRecorder)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
|
@ -35,6 +36,7 @@ import WallpaperGalleryScreen
|
|||
import TelegramStringFormatting
|
||||
import TextFormat
|
||||
import BrowserUI
|
||||
import MediaEditorScreen
|
||||
|
||||
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
|
||||
if case .default = navigation {
|
||||
|
|
@ -865,6 +867,83 @@ func openResolvedUrlImpl(
|
|||
present(controller, nil)
|
||||
}
|
||||
}
|
||||
case let .shareStory(sessionId):
|
||||
dismissInput()
|
||||
|
||||
let rootPath = context.sharedContext.applicationBindings.containerPath + "/telegram-data"
|
||||
let storiesPath = rootPath + "/share/stories/\(sessionId)"
|
||||
|
||||
var filePaths: [String] = []
|
||||
do {
|
||||
let directoryContents = try FileManager.default.contentsOfDirectory(atPath: storiesPath)
|
||||
|
||||
for item in directoryContents {
|
||||
let fullPath = storiesPath + "/" + item
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
if FileManager.default.fileExists(atPath: fullPath, isDirectory: &isDirectory) && !isDirectory.boolValue {
|
||||
filePaths.append(fullPath)
|
||||
}
|
||||
}
|
||||
filePaths.sort { lhs, rhs in
|
||||
let lhsName = ((lhs as NSString).lastPathComponent as NSString).deletingPathExtension
|
||||
let rhsName = ((rhs as NSString).lastPathComponent as NSString).deletingPathExtension
|
||||
if let lhsValue = Int(lhsName), let rhsValue = Int(rhsName) {
|
||||
return lhsValue < rhsValue
|
||||
}
|
||||
return lhsName < rhsName
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
func subject(for path: String) -> MediaEditorScreenImpl.Subject? {
|
||||
if path.hasSuffix(".jpg") {
|
||||
if let image = UIImage(contentsOfFile: path) {
|
||||
return .image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false)
|
||||
}
|
||||
} else {
|
||||
let asset = AVURLAsset(url: URL(fileURLWithPath: path))
|
||||
var dimensions = PixelDimensions(width: 1080, height: 1920)
|
||||
if let videoTrack = asset.tracks(withMediaType: .video).first {
|
||||
dimensions = PixelDimensions(videoTrack.naturalSize)
|
||||
}
|
||||
return .video(videoPath: path, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: dimensions, duration: asset.duration.seconds, videoPositionChanges: [], additionalVideoPosition: .bottomLeft, fromCamera: false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var source: Any?
|
||||
if filePaths.count > 1 {
|
||||
var subjects: [MediaEditorScreenImpl.Subject] = []
|
||||
for path in filePaths {
|
||||
if let subject = subject(for: path) {
|
||||
subjects.append(subject)
|
||||
}
|
||||
}
|
||||
source = subjects
|
||||
} else if let path = filePaths.first {
|
||||
if let subject = subject(for: path) {
|
||||
source = subject
|
||||
}
|
||||
}
|
||||
|
||||
let externalState = MediaEditorTransitionOutExternalState(
|
||||
storyTarget: nil,
|
||||
isForcedTarget: false,
|
||||
isPeerArchived: false,
|
||||
transitionOut: nil
|
||||
)
|
||||
let controller = context.sharedContext.makeStoryMediaEditorScreen(context: context, source: source, text: nil, link: nil, completion: { results, commit in
|
||||
let target: Stories.PendingTarget = results.first!.target
|
||||
externalState.storyTarget = target
|
||||
|
||||
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
}
|
||||
})
|
||||
if let navigationController {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
case let .startAttach(peerId, payload, choose):
|
||||
let presentError: (String) -> Void = { errorText in
|
||||
present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText, timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in
|
||||
|
|
|
|||
|
|
@ -1014,6 +1014,19 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
convertedUrl = "https://t.me/call/\(slug)"
|
||||
}
|
||||
}
|
||||
} else if parsedUrl.host == "shareStory" {
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
if queryItem.name == "session", let sessionId = Int64(value) {
|
||||
handleResolvedUrl(.shareStory(sessionId))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if parsedUrl.host == "stars" {
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ import InviteLinksUI
|
|||
import GiftStoreScreen
|
||||
import SendInviteLinkScreen
|
||||
import PostSuggestionsSettingsScreen
|
||||
import ForumSettingsScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
|
|
@ -2465,7 +2466,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
}
|
||||
|
||||
public func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController {
|
||||
return ChatQrCodeScreen(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary))
|
||||
return ChatQrCodeScreenImpl(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary))
|
||||
}
|
||||
|
||||
public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController {
|
||||
|
|
@ -3433,7 +3434,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
if let asset = source as? PHAsset {
|
||||
subject = .single(.asset(asset))
|
||||
} else if let image = source as? UIImage {
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|
||||
} else {
|
||||
subject = .single(.empty(PixelDimensions(width: 1080, height: 1920)))
|
||||
}
|
||||
|
|
@ -3486,7 +3487,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
subject = .single(.asset(asset))
|
||||
mode = .addingToPack
|
||||
} else if let image = source as? UIImage {
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|
||||
mode = .addingToPack
|
||||
} else if let source = source as? Signal<CameraScreenImpl.Result, NoError> {
|
||||
subject = source
|
||||
|
|
@ -3495,7 +3496,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
case .pendingImage:
|
||||
return nil
|
||||
case let .image(image):
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft)
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -3543,12 +3544,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
return editorController
|
||||
}
|
||||
|
||||
public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController {
|
||||
public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping ([MediaEditorScreenResult], @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController {
|
||||
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
|
||||
if let image = source as? UIImage {
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight))
|
||||
subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|
||||
} else if let path = source as? String {
|
||||
subject = .single(.video(videoPath: path, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: PixelDimensions(width: 1080, height: 1920), duration: 0.0, videoPositionChanges: [], additionalVideoPosition: .bottomRight))
|
||||
subject = .single(.video(videoPath: path, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: PixelDimensions(width: 1080, height: 1920), duration: 0.0, videoPositionChanges: [], additionalVideoPosition: .bottomRight, fromCamera: false))
|
||||
} else if let subjects = source as? [MediaEditorScreenImpl.Subject] {
|
||||
subject = .single(.multiple(subjects))
|
||||
} else if let subjectValue = source as? MediaEditorScreenImpl.Subject {
|
||||
subject = .single(subjectValue)
|
||||
} else {
|
||||
subject = .single(.empty(PixelDimensions(width: 1080, height: 1920)))
|
||||
}
|
||||
|
|
@ -3563,7 +3568,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
transitionOut: { finished, isNew in
|
||||
return nil
|
||||
}, completion: { results, commit in
|
||||
completion(results.first!, commit)
|
||||
completion(results, commit)
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
return editorController
|
||||
|
|
@ -3854,6 +3859,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
public func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController {
|
||||
return await PostSuggestionsSettingsScreen(context: context, peerId: peerId, completion: {})
|
||||
}
|
||||
|
||||
public func makeForumSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) -> ViewController {
|
||||
return ForumSettingsScreen(context: context, peerId: peerId)
|
||||
}
|
||||
}
|
||||
|
||||
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
||||
|
|
|
|||
|
|
@ -365,9 +365,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||
case .pendingImage:
|
||||
return nil
|
||||
case let .image(image):
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: image.additionalImage, additionalImagePosition: editorPIPPosition(image.additionalImagePosition))
|
||||
return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: image.additionalImage, additionalImagePosition: editorPIPPosition(image.additionalImagePosition), fromCamera: true)
|
||||
case let .video(video):
|
||||
return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: video.additionalVideoPath, additionalThumbnail: video.additionalCoverImage, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: video.positionChangeTimestamps, additionalVideoPosition: editorPIPPosition(video.additionalVideoPosition))
|
||||
return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: video.additionalVideoPath, additionalThumbnail: video.additionalCoverImage, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: video.positionChangeTimestamps, additionalVideoPosition: editorPIPPosition(video.additionalVideoPosition), fromCamera: true)
|
||||
case let .videoCollage(collage):
|
||||
func editorCollageItem(_ item: CameraScreenImpl.Result.VideoCollage.Item) -> MediaEditorScreenImpl.Subject.VideoCollageItem {
|
||||
let content: MediaEditorScreenImpl.Subject.VideoCollageItem.Content
|
||||
|
|
@ -392,7 +392,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||
case let .draft(draft):
|
||||
return .draft(draft, nil)
|
||||
case let .assets(assets):
|
||||
return .assets(assets)
|
||||
return .multiple(assets.map { .asset($0) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
private let arrowContainer: ASDisplayNode
|
||||
private let animatedStickerNode: DefaultAnimatedStickerNodeImpl
|
||||
private var downArrowsNode: DownArrowsIconNode?
|
||||
private var iconNode: ASImageNode?
|
||||
private var avatarNode: AvatarNode?
|
||||
private var avatarStoryIndicator: ComponentView<Empty>?
|
||||
private let textView = ComponentView<Empty>()
|
||||
|
|
@ -408,6 +409,11 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
||||
self.animatedStickerNode.dynamicColor = animationTintColor
|
||||
case let .image(image):
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode?.image = image
|
||||
self.iconNode?.contentMode = .center
|
||||
self.iconNode?.displaysAsynchronously = false
|
||||
case .downArrows:
|
||||
self.downArrowsNode = DownArrowsIconNode()
|
||||
case let .peer(peer, _):
|
||||
|
|
@ -437,7 +443,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
if let closeButtonNode = self.closeButtonNode {
|
||||
self.containerNode.addSubnode(closeButtonNode)
|
||||
}
|
||||
|
||||
if let iconNode = self.iconNode {
|
||||
self.containerNode.addSubnode(iconNode)
|
||||
}
|
||||
if let downArrowsNode = self.downArrowsNode {
|
||||
self.containerNode.addSubnode(downArrowsNode)
|
||||
}
|
||||
|
|
@ -505,6 +513,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
animationInset = 0.0
|
||||
}
|
||||
animationSpacing = 8.0
|
||||
case .image:
|
||||
animationSize = CGSize(width: 32.0, height: 32.0)
|
||||
animationInset = 0.0
|
||||
animationSpacing = 8.0
|
||||
case .peer:
|
||||
animationSize = CGSize(width: 32.0, height: 32.0)
|
||||
animationInset = 0.0
|
||||
|
|
@ -869,6 +881,11 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
let animationFrame = CGRect(origin: CGPoint(x: contentInset - animationInset, y: floorToScreenPixels((backgroundHeight - animationSize.height - animationInset * 2.0) / 2.0) + animationOffset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))
|
||||
transition.updateFrame(node: self.animatedStickerNode, frame: animationFrame)
|
||||
self.animatedStickerNode.updateLayout(size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))
|
||||
|
||||
if let iconNode = self.iconNode {
|
||||
let iconSize = CGSize(width: 32.0, height: 32.0)
|
||||
transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: animationFrame.midX - iconSize.width / 2.0, y: animationFrame.midY - iconSize.height / 2.0), size: iconSize))
|
||||
}
|
||||
|
||||
if let downArrowsNode = self.downArrowsNode {
|
||||
let arrowsSize = CGSize(width: 16.0, height: 16.0)
|
||||
|
|
@ -1068,7 +1085,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
animationDelay = delay
|
||||
case .none, .downArrows:
|
||||
animationDelay = 0.0
|
||||
case .peer:
|
||||
case .peer, .image:
|
||||
animationDelay = 0.0
|
||||
}
|
||||
|
||||
|
|
@ -1148,6 +1165,7 @@ public final class TooltipScreen: ViewController {
|
|||
|
||||
public enum Icon {
|
||||
case animation(name: String, delay: Double, tintColor: UIColor?)
|
||||
case image(UIImage)
|
||||
case peer(peer: EnginePeer, isStory: Bool)
|
||||
case downArrows
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue