Various improvements

This commit is contained in:
Ilya Laktyushin 2025-05-20 22:30:48 +02:00
parent 62f156440f
commit 81e11d6d36
102 changed files with 3073 additions and 558 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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?()
}
}
}

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@
- (TGPhotoEditorTab)toolbarTabs
{
return TGPhotoEditorCropTab | TGPhotoEditorToolsTab | TGPhotoEditorPaintTab;
return TGPhotoEditorCropTab | TGPhotoEditorToolsTab | TGPhotoEditorPaintTab | TGPhotoEditorQualityTab;
}
- (Class)viewClass

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
@interface TGDataItem : NSObject
- (instancetype)initWithData:(NSData *)data;
- (void)appendData:(NSData *)data;
- (NSData *)data;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,8 @@ public enum PermissionRequestStatus {
self = .unreachable
case .allowed:
self = .allowed
case .limited:
self = .allowed
}
}
}

View file

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

View file

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

View file

@ -44,6 +44,7 @@ swift_library(
"//submodules/SegmentedControlNode",
"//submodules/AnimatedCountLabelNode",
"//submodules/HexColor",
"//submodules/QrCodeUI",
],
visibility = [
"//visibility:public",

View file

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

View file

@ -1444,6 +1444,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .stars:
break
case .shareStory:
break
}
}
}))

View 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",
],
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,6 +25,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ICloudResources",
"//submodules/TelegramUI/Components/Resources/FetchVideoMediaResource",
"//submodules/TelegramUI/Components/Resources/FetchAudioMediaResource",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "scanqr_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "addtostory.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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