mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '140d58bdd67d3f2601c8c58cfa16acf01f4e64ae'
This commit is contained in:
commit
c06ed7062f
@ -6495,6 +6495,18 @@ Sorry for the inconvenience.";
|
||||
|
||||
"VoiceChat.Unpin" = "Unpin";
|
||||
|
||||
"VoiceChat.VideoParticipantsLimitExceeded" = "Video is only available\nfor the first 30 members";
|
||||
"VoiceChat.VideoParticipantsLimitExceeded" = "Video is only available\nfor the first %@ members";
|
||||
|
||||
"ImportStickerPack.StickerCount_1" = "1 Sticker";
|
||||
"ImportStickerPack.StickerCount_2" = "2 Stickers";
|
||||
"ImportStickerPack.StickerCount_3_10" = "%@ Stickers";
|
||||
"ImportStickerPack.StickerCount_any" = "%@ Stickers";
|
||||
"ImportStickerPack.StickerCount_many" = "%@ Stickers";
|
||||
"ImportStickerPack.StickerCount_0" = "%@ Stickers";
|
||||
"ImportStickerPack.CreateStickerSet" = "Create Sticker Set";
|
||||
"ImportStickerPack.RemoveFromImport" = "Remove From Import";
|
||||
"ImportStickerPack.ChooseName" = "Choose Name";
|
||||
"ImportStickerPack.ChooseNameDescription" = "Please choose a name for your set.";
|
||||
"ImportStickerPack.ImportingStickers" = "Importing Stickers";
|
||||
|
||||
"WallpaperPreview.PreviewBottomTextAnimatable" = "Tap the play button to view the background animation.";
|
||||
|
@ -274,6 +274,9 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
func activateMenu() {
|
||||
if self.content.menuItems().isEmpty {
|
||||
return
|
||||
}
|
||||
if case .press = self.content.menuActivation() {
|
||||
self.hapticFeedback.impact()
|
||||
}
|
||||
|
@ -3,44 +3,81 @@ import UIKit
|
||||
|
||||
public class ImportStickerPack {
|
||||
public class Sticker: Equatable {
|
||||
public enum Content {
|
||||
case image(Data)
|
||||
case animation(Data)
|
||||
}
|
||||
|
||||
public static func == (lhs: ImportStickerPack.Sticker, rhs: ImportStickerPack.Sticker) -> Bool {
|
||||
return lhs.uuid == rhs.uuid
|
||||
}
|
||||
|
||||
let image: UIImage
|
||||
let content: Content
|
||||
let emojis: [String]
|
||||
let uuid: UUID
|
||||
|
||||
init(image: UIImage, emojis: [String], uuid: UUID = UUID()) {
|
||||
self.image = image
|
||||
init(content: Content, emojis: [String], uuid: UUID = UUID()) {
|
||||
self.content = content
|
||||
self.emojis = emojis
|
||||
self.uuid = uuid
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
switch self.content {
|
||||
case let .image(data):
|
||||
return data
|
||||
case let .animation(data):
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var identifier: String
|
||||
public var name: String
|
||||
public let software: String
|
||||
public var thumbnail: String?
|
||||
public let isAnimated: Bool
|
||||
|
||||
public var stickers: [Sticker]
|
||||
public let thumbnail: Sticker?
|
||||
public let stickers: [Sticker]
|
||||
|
||||
public init?(data: Data) {
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.name = json["name"] as? String ?? ""
|
||||
self.identifier = json["identifier"] as? String ?? ""
|
||||
self.software = json["software"] as? String ?? ""
|
||||
self.isAnimated = json["isAnimated"] as? Bool ?? false
|
||||
let isAnimated = json["isAnimated"] as? Bool ?? false
|
||||
self.isAnimated = isAnimated
|
||||
|
||||
func parseSticker(_ sticker: [String: Any]) -> Sticker? {
|
||||
if let dataString = sticker["data"] as? String, let mimeType = sticker["mimeType"] as? String, let data = Data(base64Encoded: dataString) {
|
||||
var content: Sticker.Content?
|
||||
switch mimeType.lowercased() {
|
||||
case "image/png":
|
||||
if !isAnimated {
|
||||
content = .image(data)
|
||||
}
|
||||
case "application/x-tgsticker":
|
||||
if isAnimated {
|
||||
content = .animation(data)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let content = content {
|
||||
return Sticker(content: content, emojis: sticker["emojis"] as? [String] ?? [])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if let thumbnail = json["thumbnail"] as? [String: Any], let parsedSticker = parseSticker(thumbnail) {
|
||||
self.thumbnail = parsedSticker
|
||||
} else {
|
||||
self.thumbnail = nil
|
||||
}
|
||||
|
||||
var stickers: [Sticker] = []
|
||||
if let stickersArray = json["stickers"] as? [[String: Any]] {
|
||||
for sticker in stickersArray {
|
||||
if let dataString = sticker["data"] as? String, let data = Data(base64Encoded: dataString), let image = UIImage(data: data) {
|
||||
stickers.append(Sticker(image: image, emojis: sticker["emojis"] as? [String] ?? []))
|
||||
if let parsedSticker = parseSticker(sticker) {
|
||||
stickers.append(parsedSticker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +68,9 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
|
||||
self.controllerNode.cancel = { [weak self] in
|
||||
self?.dismiss()
|
||||
}
|
||||
self.controllerNode.present = { [weak self] controller, arguments in
|
||||
self?.present(controller, in: .window(.root), with: arguments)
|
||||
}
|
||||
self.controllerNode.presentInGlobalOverlay = { [weak self] controller, arguments in
|
||||
self?.presentInGlobalOverlay(controller, with: arguments)
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
|
||||
private var interaction: StickerPackPreviewInteraction!
|
||||
|
||||
var present: ((ViewController, Any?) -> Void)?
|
||||
var presentInGlobalOverlay: ((ViewController, Any?) -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
var cancel: (() -> Void)?
|
||||
@ -75,10 +76,13 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
let ready = Promise<Bool>()
|
||||
private var didSetReady = false
|
||||
|
||||
private var pendingItems: [StickerPackPreviewGridEntry] = []
|
||||
private var currentItems: [StickerPackPreviewGridEntry] = []
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -151,6 +155,10 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
@ -160,31 +168,23 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in
|
||||
if let strongSelf = self {
|
||||
if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem {
|
||||
// var menuItems: [PeekControllerMenuItem] = []
|
||||
// if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
|
||||
// if strongSelf.sendSticker != nil {
|
||||
// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
|
||||
// if let strongSelf = self {
|
||||
// return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
// menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in
|
||||
// if let strongSelf = self {
|
||||
// if isStarred {
|
||||
// let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
|
||||
// } else {
|
||||
// let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// }))
|
||||
// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }))
|
||||
// }
|
||||
// return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems))
|
||||
return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: [])))
|
||||
var menuItems: [ContextMenuItem] = []
|
||||
if strongSelf.currentItems.count > 1 {
|
||||
menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ImportStickerPack_RemoveFromImport, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
if let strongSelf = self {
|
||||
var updatedItems = strongSelf.currentItems
|
||||
updatedItems.removeAll(where: { $0.stickerItem.uuid == item.uuid })
|
||||
strongSelf.pendingItems = updatedItems
|
||||
|
||||
if let (layout, navigationHeight) = strongSelf.containerLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -304,21 +304,16 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
var itemCount = 0
|
||||
var animateIn = false
|
||||
|
||||
if let stickerPack = self.stickerPack {
|
||||
var updatedItems: [StickerPackPreviewGridEntry] = []
|
||||
for item in stickerPack.stickers {
|
||||
updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item))
|
||||
}
|
||||
if let _ = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count {
|
||||
let previousItems = self.currentItems
|
||||
self.currentItems = self.pendingItems
|
||||
|
||||
if self.currentItems.isEmpty && !updatedItems.isEmpty {
|
||||
let entities = generateTextEntities(stickerPack.name, enabledTypes: [.mention])
|
||||
let font = Font.medium(20.0)
|
||||
self.contentTitleNode.attributedText = stringWithAppliedEntities(stickerPack.name, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font)
|
||||
animateIn = true
|
||||
itemCount = updatedItems.count
|
||||
}
|
||||
transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme)
|
||||
self.currentItems = updatedItems
|
||||
let titleFont = Font.medium(20.0)
|
||||
self.contentTitleNode.attributedText = stringWithAppliedEntities(self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count)), entities: [], baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont)
|
||||
animateIn = true
|
||||
itemCount = self.currentItems.count
|
||||
|
||||
transaction = StickerPackPreviewGridTransaction(previousList: previousItems, list: self.currentItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme)
|
||||
}
|
||||
|
||||
let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
@ -421,7 +416,45 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
}
|
||||
|
||||
@objc func installActionButtonPressed() {
|
||||
|
||||
let controller = importStickerPackTitleController(sharedContext: self.context.sharedContext, account: self.context.account, title: self.presentationData.strings.ImportStickerPack_ChooseName, text: self.presentationData.strings.ImportStickerPack_ChooseNameDescription, placeholder: "", doneButtonTitle: nil, value: nil, maxLength: 128, apply: { [weak self] title in
|
||||
if let strongSelf = self, let stickerPack = strongSelf.stickerPack, var title = title {
|
||||
title = title.trimmingTrailingSpaces()
|
||||
let shortName = title.replacingOccurrences(of: " ", with: "") + "_by_laktyushin"
|
||||
var stickers: [ImportSticker] = []
|
||||
for item in strongSelf.currentItems {
|
||||
var dimensions = PixelDimensions(width: 512, height: 512)
|
||||
if case let .image(data) = item.stickerItem.content, let image = UIImage(data: data) {
|
||||
dimensions = PixelDimensions(image.size)
|
||||
}
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.stickerItem.data)
|
||||
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions))
|
||||
}
|
||||
var thumbnailSticker: ImportSticker?
|
||||
if let thumbnail = stickerPack.thumbnail {
|
||||
var dimensions = PixelDimensions(width: 512, height: 512)
|
||||
if case let .image(data) = thumbnail.content, let image = UIImage(data: data) {
|
||||
dimensions = PixelDimensions(image.size)
|
||||
}
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data)
|
||||
thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions)
|
||||
}
|
||||
|
||||
strongSelf.disposable.set(createStickerSet(account: strongSelf.context.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnailSticker, isAnimated: stickerPack.isAnimated).start(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
if case let .complete(pack, items) = status {
|
||||
print("done!")
|
||||
}
|
||||
}
|
||||
}, error: { error in
|
||||
if let strongSelf = self {
|
||||
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
self.present?(controller, nil)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -461,13 +494,18 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
|
||||
func updateStickerPack(_ stickerPack: ImportStickerPack) {
|
||||
self.stickerPack = stickerPack
|
||||
var updatedItems: [StickerPackPreviewGridEntry] = []
|
||||
for item in stickerPack.stickers {
|
||||
updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item))
|
||||
}
|
||||
self.pendingItems = updatedItems
|
||||
|
||||
// self.interaction.playAnimatedStickers = stickerSettings.loopAnimatedStickers
|
||||
|
||||
if let _ = self.containerLayout {
|
||||
self.dequeueUpdateStickerPack()
|
||||
}
|
||||
self.installActionButtonNode.setTitle("Create Sticker Set", with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
self.installActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateStickerSet, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
// switch stickerPack {
|
||||
// case .none, .fetching:
|
||||
// self.installActionSeparatorNode.alpha = 0.0
|
||||
|
@ -0,0 +1,468 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import UrlEscaping
|
||||
|
||||
private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
private let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
private let clearButton: HighlightableButtonNode
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
var textChanged: ((String) -> Void)?
|
||||
|
||||
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textInputNode.attributedText?.string ?? ""
|
||||
}
|
||||
set {
|
||||
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
if self.textInputNode.isFirstResponder() {
|
||||
self.clearButton.isHidden = newValue.isEmpty
|
||||
} else {
|
||||
self.clearButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String = "" {
|
||||
didSet {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
}
|
||||
}
|
||||
|
||||
private let maxLength: Int
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) {
|
||||
self.theme = theme
|
||||
self.maxLength = maxLength
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
|
||||
self.textInputNode = EditableTextNode()
|
||||
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.clipsToBounds = true
|
||||
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
|
||||
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.keyboardType = .default
|
||||
self.textInputNode.autocapitalizationType = .sentences
|
||||
self.textInputNode.returnKeyType = returnKeyType
|
||||
self.textInputNode.autocorrectionType = .default
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
self.clearButton = HighlightableButtonNode()
|
||||
self.clearButton.imageNode.displaysAsynchronously = false
|
||||
self.clearButton.imageNode.displayWithoutProcessing = true
|
||||
self.clearButton.displaysAsynchronously = false
|
||||
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.delegate = self
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
self.addSubnode(self.clearButton)
|
||||
|
||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: [])
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height)))
|
||||
|
||||
if let image = self.clearButton.image(for: []) {
|
||||
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size))
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.textInputNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
self.textInputNode.resignFirstResponder()
|
||||
}
|
||||
|
||||
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(editableTextNode.textView.text)
|
||||
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
|
||||
self.clearButton.isHidden = !self.placeholderNode.isHidden
|
||||
}
|
||||
|
||||
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty
|
||||
}
|
||||
|
||||
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
|
||||
self.clearButton.isHidden = true
|
||||
}
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
|
||||
if updatedText.count > maxLength {
|
||||
self.textInputNode.layer.addShakeAnimation()
|
||||
return false
|
||||
}
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
self.placeholderNode.isHidden = false
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
self.textInputNode.attributedText = nil
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
private final class ImportStickerPackTitleAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
let inputFieldNode: ImportStickerPackTitleInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 8
|
||||
|
||||
self.inputFieldNode = ImportStickerPackTitleInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength)
|
||||
self.inputFieldNode.text = value ?? ""
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var value: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
func importStickerPackTitleController(sharedContext: SharedAccountContext, account: Account, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController {
|
||||
let presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: {
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = ImportStickerPackTitleAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
dismissImpl?(true)
|
||||
|
||||
let previousValue = value ?? ""
|
||||
let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
apply(previousValue != newValue || value == nil ? newValue : nil)
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = {
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller, weak contentNode] animated in
|
||||
contentNode?.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
@ -57,12 +57,10 @@ final class StickerPackPreviewGridItem: GridItem {
|
||||
private let textFont = Font.regular(20.0)
|
||||
|
||||
final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
private var currentState: (Account, ImportStickerPack.Sticker?)?
|
||||
private var currentState: (Account, ImportStickerPack.Sticker?, CGSize)?
|
||||
private var isEmpty: Bool?
|
||||
// private let imageNode: TransformImageNode
|
||||
private let imageNode: ASImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
@ -86,50 +84,16 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
|
||||
override init() {
|
||||
self.imageNode = ASImageNode()
|
||||
// self.imageNode = TransformImageNode()
|
||||
// self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
self.placeholderNode?.isUserInteractionEnabled = false
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
// self.addSubnode(placeholderNode)
|
||||
}
|
||||
|
||||
var firstTime = true
|
||||
// self.imageNode.imageUpdated = { [weak self] image in
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// if image != nil {
|
||||
// strongSelf.removePlaceholder(animated: !firstTime)
|
||||
// }
|
||||
// firstTime = false
|
||||
// }
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func removePlaceholder(animated: Bool) {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.placeholderNode = nil
|
||||
if !animated {
|
||||
placeholderNode.removeFromSupernode()
|
||||
} else {
|
||||
placeholderNode.allowsGroupOpacity = true
|
||||
placeholderNode.alpha = 0.0
|
||||
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
|
||||
placeholderNode?.removeFromSupernode()
|
||||
placeholderNode?.allowsGroupOpacity = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
@ -141,47 +105,34 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
self.theme = theme
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 !== stickerItem || self.isEmpty != isEmpty {
|
||||
var dimensions = CGSize(width: 512.0, height: 512.0)
|
||||
if let stickerItem = stickerItem {
|
||||
self.imageNode.image = stickerItem.image
|
||||
// if stickerItem.file.isAnimatedSticker {
|
||||
// let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
// self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
//
|
||||
// if self.animationNode == nil {
|
||||
// let animationNode = AnimatedStickerNode()
|
||||
// self.animationNode = animationNode
|
||||
// self.addSubnode(animationNode)
|
||||
// animationNode.started = { [weak self] in
|
||||
// self?.imageNode.isHidden = true
|
||||
// self?.removePlaceholder(animated: false)
|
||||
// }
|
||||
// }
|
||||
// let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
// self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
// self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
// } else {
|
||||
// if let animationNode = self.animationNode {
|
||||
// animationNode.visibility = false
|
||||
// self.animationNode = nil
|
||||
// animationNode.removeFromSupernode()
|
||||
// }
|
||||
// self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
// }
|
||||
} else {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
if isEmpty {
|
||||
if !placeholderNode.alpha.isZero {
|
||||
placeholderNode.alpha = 0.0
|
||||
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
switch stickerItem.content {
|
||||
case let .image(data):
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
}
|
||||
} else {
|
||||
placeholderNode.alpha = 1.0
|
||||
}
|
||||
self.imageNode.isHidden = false
|
||||
if let image = UIImage(data: data) {
|
||||
self.imageNode.image = image
|
||||
dimensions = image.size
|
||||
}
|
||||
case let .animation(data):
|
||||
self.imageNode.isHidden = true
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
|
||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
// animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
}
|
||||
} else {
|
||||
dimensions = CGSize()
|
||||
}
|
||||
self.currentState = (account, stickerItem)
|
||||
self.currentState = (account, stickerItem, dimensions)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
self.isEmpty = isEmpty
|
||||
@ -194,32 +145,13 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0)
|
||||
let boundingSize = CGSize(width: boundsSide, height: boundsSide)
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize)
|
||||
placeholderNode.frame = bounds
|
||||
|
||||
// if let theme = self.theme, let (_, stickerItem) = self.currentState, let item = stickerItem {
|
||||
// placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size)
|
||||
// }
|
||||
}
|
||||
|
||||
self.imageNode.frame = bounds
|
||||
// if let (_, item) = self.currentState {
|
||||
// if let item = item, let dimensions = item.file.dimensions?.cgSize {
|
||||
// let imageSize = dimensions.aspectFitted(boundingSize)
|
||||
// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
// self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
// if let animationNode = self.animationNode {
|
||||
// animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
// animationNode.updateLayout(size: imageSize)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
|
||||
if let (_, _, dimensions) = self.currentState {
|
||||
let imageSize = dimensions.aspectFitted(boundingSize)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +164,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
var isPreviewing = false
|
||||
if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem {
|
||||
if let (_, maybeItem, _) = self.currentState, let interaction = self.interaction, let item = maybeItem {
|
||||
isPreviewing = interaction.previewedItem === item
|
||||
}
|
||||
if self.currentIsPreviewing != isPreviewing {
|
||||
|
@ -68,15 +68,11 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
self.textNode = ASTextNode()
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.image = item.image
|
||||
|
||||
if case let .image(data) = item.content, let image = UIImage(data: data) {
|
||||
self.imageNode.image = image
|
||||
}
|
||||
self.textNode.attributedText = NSAttributedString(string: item.emojis.joined(separator: " "), font: Font.regular(32.0), textColor: .black)
|
||||
|
||||
// for case let .Sticker(text, _, _) in item.file.attributes {
|
||||
// self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black)
|
||||
// break
|
||||
// }
|
||||
|
||||
|
||||
// if item.file.isAnimatedSticker {
|
||||
// let animationNode = AnimatedStickerNode()
|
||||
// self.animationNode = animationNode
|
||||
@ -92,10 +88,6 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
// self.animationNode = nil
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false, fetched: true))
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
@ -330,8 +330,13 @@
|
||||
- (void)updateWithFetchResult:(TGMediaAssetFetchResult *)fetchResult
|
||||
{
|
||||
TGMediaAsset *currentAsset = ((TGMediaPickerGalleryItem *)_galleryController.currentItem).asset;
|
||||
bool exists = ([fetchResult indexOfAsset:currentAsset] != NSNotFound);
|
||||
|
||||
bool exists;
|
||||
if ([currentAsset isKindOfClass:[TGCameraCapturedVideo class]]) {
|
||||
exists = [fetchResult indexOfAsset:((TGCameraCapturedVideo *)currentAsset).originalAsset] != NSNotFound;
|
||||
} else {
|
||||
exists = ([fetchResult indexOfAsset:currentAsset] != NSNotFound);
|
||||
}
|
||||
if (!exists)
|
||||
{
|
||||
_galleryModel.dismiss(true, false);
|
||||
|
@ -239,6 +239,15 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
||||
}))
|
||||
}
|
||||
|
||||
options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: {
|
||||
let coordinates = "\(lon),\(lat)"
|
||||
if withDirections {
|
||||
return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go")
|
||||
} else {
|
||||
return .openUrl(url: "dgis://2gis.ru/geo/\(coordinates)")
|
||||
}
|
||||
}))
|
||||
|
||||
options.append(OpenInOption(identifier: "moovit", application: .other(title: "Moovit", identifier: 498477945, scheme: "moovit", store: nil), action: {
|
||||
if withDirections {
|
||||
let destName: String
|
||||
|
@ -44,6 +44,26 @@ let bottomAreaHeight: CGFloat = 206.0
|
||||
private let fullscreenBottomAreaHeight: CGFloat = 80.0
|
||||
private let bottomGradientHeight: CGFloat = 70.0
|
||||
|
||||
public struct VoiceChatConfiguration {
|
||||
static var defaultValue: VoiceChatConfiguration {
|
||||
return VoiceChatConfiguration(videoParticipantsMaxCount: 30)
|
||||
}
|
||||
|
||||
public let videoParticipantsMaxCount: Int32
|
||||
|
||||
fileprivate init(videoParticipantsMaxCount: Int32) {
|
||||
self.videoParticipantsMaxCount = videoParticipantsMaxCount
|
||||
}
|
||||
|
||||
static func with(appConfiguration: AppConfiguration) -> VoiceChatConfiguration {
|
||||
if let data = appConfiguration.data, let value = data["groupcall_video_participants_max"] as? Double {
|
||||
return VoiceChatConfiguration(videoParticipantsMaxCount: Int32(value))
|
||||
} else {
|
||||
return .defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decorationCornersImage(top: Bool, bottom: Bool, dark: Bool) -> UIImage? {
|
||||
if !top && !bottom {
|
||||
return nil
|
||||
@ -88,8 +108,6 @@ func decorationTopCornersImage(dark: Bool) -> UIImage? {
|
||||
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 32)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func decorationBottomCornersImage(dark: Bool) -> UIImage? {
|
||||
return generateImage(CGSize(width: 50.0, height: 110.0), rotatedContext: { (size, context) in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
@ -491,7 +509,7 @@ public final class VoiceChatController: ViewController {
|
||||
text = .text(about, textIcon, .generic)
|
||||
}
|
||||
|
||||
return VoiceChatTileItem(account: context.account, peer: peerEntry.peer, videoEndpointId: videoEndpointId, videoReady: videoReady, videoTimeouted: videoTimeouted, isVideoLimit: false, isPaused: videoIsPaused, isOwnScreencast: peerEntry.presentationEndpointId == videoEndpointId && peerEntry.isMyPeer, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, secondary: secondary, isTablet: isTablet, icon: showAsPresentation ? .presentation : icon, text: text, additionalText: additionalText, action: {
|
||||
return VoiceChatTileItem(account: context.account, peer: peerEntry.peer, videoEndpointId: videoEndpointId, videoReady: videoReady, videoTimeouted: videoTimeouted, isVideoLimit: false, videoLimit: 0, isPaused: videoIsPaused, isOwnScreencast: peerEntry.presentationEndpointId == videoEndpointId && peerEntry.isMyPeer, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, secondary: secondary, isTablet: isTablet, icon: showAsPresentation ? .presentation : icon, text: text, additionalText: additionalText, action: {
|
||||
interaction.switchToPeer(peer.id, videoEndpointId, !secondary)
|
||||
}, contextAction: { node, gesture in
|
||||
interaction.peerContextAction(peerEntry, node, gesture, false)
|
||||
@ -749,6 +767,8 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
|
||||
|
||||
private var configuration: VoiceChatConfiguration?
|
||||
|
||||
private weak var controller: VoiceChatController?
|
||||
private let sharedContext: SharedAccountContext
|
||||
private let context: AccountContext
|
||||
@ -1863,16 +1883,21 @@ public final class VoiceChatController: ViewController {
|
||||
self.call.state,
|
||||
self.call.members,
|
||||
invitedPeers,
|
||||
self.displayAsPeersPromise.get()
|
||||
self.displayAsPeersPromise.get(),
|
||||
self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
||||
)
|
||||
|> mapToThrottled { values in
|
||||
return .single(values)
|
||||
|> then(.complete() |> delay(0.1, queue: Queue.mainQueue()))
|
||||
}).start(next: { [weak self] state, callMembers, invitedPeers, displayAsPeers in
|
||||
}).start(next: { [weak self] state, callMembers, invitedPeers, displayAsPeers, preferencesView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||
let configuration = VoiceChatConfiguration.with(appConfiguration: appConfiguration)
|
||||
strongSelf.configuration = configuration
|
||||
|
||||
var animate = false
|
||||
if strongSelf.callState != state {
|
||||
if let previousCallState = strongSelf.callState {
|
||||
@ -2461,14 +2486,18 @@ public final class VoiceChatController: ViewController {
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
if let strongSelf = self {
|
||||
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
|
||||
}
|
||||
})))
|
||||
let isScheduled = strongSelf.isScheduled
|
||||
|
||||
if !isScheduled {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_NoiseSuppression, textColor: .primary, textLayout: .secondLineWithValue(strongSelf.isNoiseSuppressionEnabled ? strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionEnabled : strongSelf.presentationData.strings.VoiceChat_NoiseSuppressionDisabled), icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
if let strongSelf = self {
|
||||
strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled)
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if let callState = strongSelf.callState, callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
|
||||
if #available(iOS 12.0, *) {
|
||||
@ -2544,7 +2573,6 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
if canManageCall {
|
||||
let isScheduled = strongSelf.isScheduled
|
||||
items.append(.action(ContextMenuActionItem(text: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelVoiceChat : strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { _, f in
|
||||
@ -4945,7 +4973,8 @@ public final class VoiceChatController: ViewController {
|
||||
tileItems.removeAll()
|
||||
gridTileItems.removeAll()
|
||||
|
||||
tileItems.append(VoiceChatTileItem(account: self.context.account, peer: peer, videoEndpointId: "", videoReady: false, videoTimeouted: true, isVideoLimit: true, isPaused: false, isOwnScreencast: false, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: false, secondary: false, isTablet: false, icon: .none, text: .none, additionalText: nil, action: {}, contextAction: nil, getVideo: { _ in return nil }, getAudioLevel: nil))
|
||||
let configuration = self.configuration ?? VoiceChatConfiguration.defaultValue
|
||||
tileItems.append(VoiceChatTileItem(account: self.context.account, peer: peer, videoEndpointId: "", videoReady: false, videoTimeouted: true, isVideoLimit: true, videoLimit: configuration.videoParticipantsMaxCount, isPaused: false, isOwnScreencast: false, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: false, secondary: false, isTablet: false, icon: .none, text: .none, additionalText: nil, action: {}, contextAction: nil, getVideo: { _ in return nil }, getAudioLevel: nil))
|
||||
}
|
||||
|
||||
for member in callMembers.0 {
|
||||
|
@ -32,9 +32,8 @@ private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
private let borderLineWidth: CGFloat = 2.0
|
||||
|
||||
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
let fadeHeight: CGFloat = 50.0
|
||||
|
||||
private var fadeImage: UIImage? = {
|
||||
return generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in
|
||||
@ -185,7 +184,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
private var wavesColor: UIColor?
|
||||
|
||||
let videoContainerNode: ASDisplayNode
|
||||
private let videoFadeNode: ASDisplayNode
|
||||
let videoFadeNode: ASDisplayNode
|
||||
var videoNode: GroupVideoNode?
|
||||
|
||||
private var profileNode: VoiceChatPeerProfileNode?
|
||||
@ -844,7 +843,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if !strongSelf.isExtracted && !strongSelf.animatingExtraction {
|
||||
if !strongSelf.isExtracted && !strongSelf.animatingExtraction && strongSelf.videoContainerNode.supernode == strongSelf.offsetContainerNode {
|
||||
strongSelf.videoFadeNode.frame = CGRect(x: 0.0, y: videoSize.height - fadeHeight, width: videoSize.width, height: fadeHeight)
|
||||
strongSelf.videoContainerNode.bounds = CGRect(origin: CGPoint(), size: videoSize)
|
||||
|
||||
|
@ -23,7 +23,6 @@ import ContextUI
|
||||
private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white)
|
||||
private let backgroundCornerRadius: CGFloat = 11.0
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
private class VoiceChatPinButtonNode: HighlightTrackingButtonNode {
|
||||
|
@ -35,8 +35,6 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let statusNode: VoiceChatParticipantStatusNode
|
||||
|
||||
private var videoNode: GroupVideoNode?
|
||||
|
||||
private var appeared = false
|
||||
|
||||
init(context: AccountContext, size: CGSize, sourceSize: CGSize, peer: Peer, text: VoiceChatParticipantItem.ParticipantText, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError>, requestDismiss: (() -> Void)?) {
|
||||
@ -300,10 +298,13 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
||||
transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0)
|
||||
|
||||
let initialRect: CGRect
|
||||
let hasVideo: Bool
|
||||
if let videoNode = sourceNode.videoNode, videoNode.supernode == sourceNode.videoContainerNode, !videoNode.alpha.isZero {
|
||||
initialRect = sourceRect
|
||||
hasVideo = true
|
||||
} else {
|
||||
initialRect = sourceNode.avatarNode.frame
|
||||
hasVideo = false
|
||||
}
|
||||
let initialScale = initialRect.width / targetRect.width
|
||||
|
||||
@ -320,41 +321,29 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
||||
appearanceTransition = .animated(duration: springDuration, curve: .customSpring(damping: springDamping, initialVelocity: 0.0))
|
||||
}
|
||||
|
||||
if let videoNode = sourceNode.videoNode {
|
||||
if let videoNode = sourceNode.videoNode, hasVideo {
|
||||
videoNode.updateLayout(size: targetSize, layoutMode: .fillOrFitToSquare, transition: appearanceTransition)
|
||||
appearanceTransition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize))
|
||||
appearanceTransition.updateFrame(node: sourceNode.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - fadeHeight), size: CGSize(width: targetSize.width, height: fadeHeight)))
|
||||
appearanceTransition.updateTransformScale(node: sourceNode.videoContainerNode, scale: 1.0)
|
||||
appearanceTransition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius)))
|
||||
sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius
|
||||
appearanceTransition.updateAlpha(node: sourceNode.videoFadeNode, alpha: 0.0)
|
||||
} else {
|
||||
let transitionNode = ASImageNode()
|
||||
transitionNode.clipsToBounds = true
|
||||
transitionNode.displaysAsynchronously = false
|
||||
transitionNode.displayWithoutProcessing = true
|
||||
transitionNode.image = sourceNode.avatarNode.unroundedImage
|
||||
transitionNode.frame = CGRect(origin: CGPoint(), size: targetSize)
|
||||
transitionNode.cornerRadius = targetRect.width / 2.0
|
||||
radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0)
|
||||
|
||||
sourceNode.avatarNode.isHidden = true
|
||||
self.avatarListWrapperNode.contentNode.insertSubnode(transitionNode, at: 0)
|
||||
}
|
||||
self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode)
|
||||
|
||||
let transitionNode = ASImageNode()
|
||||
transitionNode.clipsToBounds = true
|
||||
transitionNode.displaysAsynchronously = false
|
||||
transitionNode.displayWithoutProcessing = true
|
||||
transitionNode.image = sourceNode.avatarNode.unroundedImage
|
||||
transitionNode.frame = CGRect(origin: CGPoint(), size: targetSize)
|
||||
transitionNode.cornerRadius = targetRect.width / 2.0
|
||||
radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0)
|
||||
|
||||
sourceNode.avatarNode.isHidden = true
|
||||
self.avatarListWrapperNode.contentNode.insertSubnode(transitionNode, at: 0)
|
||||
// if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
// self.videoFadeNode.image = sourceNode.fadeNode.image
|
||||
// self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height)
|
||||
//
|
||||
// self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode)
|
||||
// self.view.insertSubview(snapshotView, aboveSubview: sourceNode.videoContainerNode.view)
|
||||
// snapshotView.frame = sourceRect
|
||||
// transition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - snapshotView.frame.size.height), size: snapshotView.frame.size))
|
||||
// snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
// snapshotView.removeFromSuperview()
|
||||
// })
|
||||
// transition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetSize.width, height: self.videoFadeNode.frame.height)))
|
||||
// self.videoFadeNode.alpha = 0.0
|
||||
// self.videoFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
// }
|
||||
|
||||
self.avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
|
||||
self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
@ -450,6 +439,8 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
||||
self.infoNode.alpha = 0.0
|
||||
self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
} else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode {
|
||||
let backgroundTargetRect = targetRect
|
||||
|
||||
let initialSize = self.bounds
|
||||
self.updateInfo(size: targetRect.size, sourceSize: targetRect.size, animate: true)
|
||||
|
||||
@ -457,7 +448,15 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
||||
|
||||
transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius)
|
||||
|
||||
let targetScale = targetRect.width / avatarListContainerNode.frame.width
|
||||
var targetRect = targetRect
|
||||
let hasVideo: Bool
|
||||
if let videoNode = targetNode.videoNode, !videoNode.alpha.isZero {
|
||||
hasVideo = true
|
||||
} else {
|
||||
targetRect = targetNode.avatarNode.frame
|
||||
hasVideo = false
|
||||
}
|
||||
let targetScale = targetRect.width / self.avatarListContainerNode.frame.width
|
||||
|
||||
self.insertSubnode(targetNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode)
|
||||
self.insertSubnode(self.videoFadeNode, aboveSubnode: targetNode.videoContainerNode)
|
||||
@ -474,24 +473,16 @@ final class VoiceChatPeerProfileNode: ASDisplayNode {
|
||||
|
||||
radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: backgroundCornerRadius)
|
||||
|
||||
// if let snapshotView = targetNode.infoNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
// self.view.insertSubview(snapshotView, aboveSubview: targetNode.videoContainerNode.view)
|
||||
// let snapshotFrame = snapshotView.frame
|
||||
// snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: initialSize.width - snapshotView.frame.size.height), size: snapshotView.frame.size)
|
||||
// transition.updateFrame(view: snapshotView, frame: snapshotFrame)
|
||||
// snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
// transition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetRect.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetRect.width, height: self.videoFadeNode.frame.height)))
|
||||
// self.videoFadeNode.alpha = 1.0
|
||||
// self.videoFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
// }
|
||||
|
||||
if false, let videoNode = targetNode.videoNode {
|
||||
videoNode.updateLayout(size: targetRect.size, layoutMode: .fillOrFitToSquare, transition: transition)
|
||||
transition.updateFrame(node: videoNode, frame: targetRect)
|
||||
transition.updateFrame(node: targetNode.videoContainerNode, frame: targetRect)
|
||||
if hasVideo, let videoNode = targetNode.videoNode {
|
||||
videoNode.updateLayout(size: CGSize(width: 180.0, height: 180.0), layoutMode: .fillOrFitToSquare, transition: transition)
|
||||
transition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 180.0, height: 180.0)))
|
||||
transition.updateTransformScale(node: targetNode.videoContainerNode, scale: 84.0 / 180.0)
|
||||
transition.updateFrameAsPositionAndBounds(node: targetNode.videoContainerNode, frame: CGRect(x: 0.0, y: 0.0, width: 180.0, height: 180.0))
|
||||
transition.updatePosition(node: targetNode.videoContainerNode, position: CGPoint(x: 42.0, y: 42.0))
|
||||
transition.updateFrame(node: targetNode.videoFadeNode, frame: CGRect(x: 0.0, y: 180.0 - fadeHeight, width: 180.0, height: fadeHeight))
|
||||
transition.updateAlpha(node: targetNode.videoFadeNode, alpha: 1.0)
|
||||
}
|
||||
|
||||
let backgroundTargetRect = targetRect
|
||||
let initialBackgroundPosition = self.backgroundImageNode.position
|
||||
self.backgroundImageNode.layer.position = backgroundTargetRect.center
|
||||
let initialBackgroundBounds = self.backgroundImageNode.bounds
|
||||
|
@ -29,6 +29,7 @@ final class VoiceChatTileItem: Equatable {
|
||||
let videoReady: Bool
|
||||
let videoTimeouted: Bool
|
||||
let isVideoLimit: Bool
|
||||
let videoLimit: Int32
|
||||
let isPaused: Bool
|
||||
let isOwnScreencast: Bool
|
||||
let strings: PresentationStrings
|
||||
@ -48,13 +49,14 @@ final class VoiceChatTileItem: Equatable {
|
||||
return self.videoEndpointId
|
||||
}
|
||||
|
||||
init(account: Account, peer: Peer, videoEndpointId: String, videoReady: Bool, videoTimeouted: Bool, isVideoLimit: Bool, isPaused: Bool, isOwnScreencast: Bool, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, secondary: Bool, isTablet: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, additionalText: VoiceChatParticipantItem.ParticipantText?, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, getVideo: @escaping (GroupVideoNode.Position) -> GroupVideoNode?, getAudioLevel: (() -> Signal<Float, NoError>)?) {
|
||||
init(account: Account, peer: Peer, videoEndpointId: String, videoReady: Bool, videoTimeouted: Bool, isVideoLimit: Bool, videoLimit: Int32, isPaused: Bool, isOwnScreencast: Bool, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, secondary: Bool, isTablet: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, additionalText: VoiceChatParticipantItem.ParticipantText?, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, getVideo: @escaping (GroupVideoNode.Position) -> GroupVideoNode?, getAudioLevel: (() -> Signal<Float, NoError>)?) {
|
||||
self.account = account
|
||||
self.peer = peer
|
||||
self.videoEndpointId = videoEndpointId
|
||||
self.videoReady = videoReady
|
||||
self.videoTimeouted = videoTimeouted
|
||||
self.isVideoLimit = isVideoLimit
|
||||
self.videoLimit = videoLimit
|
||||
self.isPaused = isPaused
|
||||
self.isOwnScreencast = isOwnScreencast
|
||||
self.strings = strings
|
||||
@ -113,7 +115,6 @@ final class VoiceChatTileItem: Equatable {
|
||||
}
|
||||
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let fadeHeight: CGFloat = 50.0
|
||||
|
||||
var tileFadeImage: UIImage? = {
|
||||
return generateImage(CGSize(width: fadeHeight, height: fadeHeight), rotatedContext: { size, context in
|
||||
@ -412,8 +413,8 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
||||
|
||||
var showPlaceholder = false
|
||||
if item.isVideoLimit {
|
||||
self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_VideoParticipantsLimitExceeded, font: Font.semibold(13.0), textColor: .white)
|
||||
self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pause"), color: .white)
|
||||
self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_VideoParticipantsLimitExceeded(String(item.videoLimit)).0, font: Font.semibold(13.0), textColor: .white)
|
||||
self.placeholderIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/VideoUnavailable"), color: .white)
|
||||
showPlaceholder = true
|
||||
} else if item.isOwnScreencast {
|
||||
self.placeholderTextNode.attributedText = NSAttributedString(string: item.strings.VoiceChat_YouAreSharingScreen, font: Font.semibold(13.0), textColor: .white)
|
||||
@ -580,7 +581,8 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
||||
let placeholderTextSize = self.placeholderTextNode.updateLayout(CGSize(width: size.width - 30.0, height: 100.0))
|
||||
transition.updateFrame(node: self.placeholderTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - placeholderTextSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) + 10.0), size: placeholderTextSize))
|
||||
if let image = self.placeholderIconNode.image {
|
||||
let imageSize = CGSize(width: image.size.width * 0.5, height: image.size.height * 0.5)
|
||||
let imageScale: CGFloat = item.isVideoLimit ? 1.0 : 0.5
|
||||
let imageSize = CGSize(width: image.size.width * imageScale, height: image.size.height * imageScale)
|
||||
transition.updateFrame(node: self.placeholderIconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels(size.height / 2.0) - imageSize.height - 4.0), size: imageSize))
|
||||
}
|
||||
}
|
||||
|
209
submodules/TelegramCore/Sources/ImportStickers.swift
Normal file
209
submodules/TelegramCore/Sources/ImportStickers.swift
Normal file
@ -0,0 +1,209 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
|
||||
import SyncCore
|
||||
|
||||
private enum UploadStickerStatus {
|
||||
case progress(Float)
|
||||
case complete(TelegramMediaFile)
|
||||
}
|
||||
|
||||
private enum UploadStickerError {
|
||||
case generic
|
||||
}
|
||||
|
||||
private struct UploadedStickerData {
|
||||
fileprivate let resource: MediaResource
|
||||
fileprivate let content: UploadedStickerDataContent
|
||||
}
|
||||
|
||||
private enum UploadedStickerDataContent {
|
||||
case result(MultipartUploadResult)
|
||||
case error
|
||||
}
|
||||
|
||||
private func uploadedSticker(postbox: Postbox, network: Network, resource: MediaResource) -> Signal<UploadedStickerData, NoError> {
|
||||
return multipartUpload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .file), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
|
||||
|> map { result -> UploadedStickerData in
|
||||
return UploadedStickerData(resource: resource, content: .result(result))
|
||||
}
|
||||
|> `catch` { _ -> Signal<UploadedStickerData, NoError> in
|
||||
return .single(UploadedStickerData(resource: resource, content: .error))
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadSticker(account: Account, peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, isAnimated: Bool) -> Signal<UploadStickerStatus, UploadStickerError> {
|
||||
guard let inputPeer = apiInputPeer(peer) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
return uploadedSticker(postbox: account.postbox, network: account.network, resource: resource)
|
||||
|> mapError { _ -> UploadStickerError in return .generic }
|
||||
|> mapToSignal { result -> Signal<UploadStickerStatus, UploadStickerError> in
|
||||
switch result.content {
|
||||
case .error:
|
||||
return .fail(.generic)
|
||||
case let .result(resultData):
|
||||
switch resultData {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case let .inputFile(file):
|
||||
var flags: Int32 = 0
|
||||
flags |= (1 << 4)
|
||||
var attributes: [Api.DocumentAttribute] = []
|
||||
attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil))
|
||||
attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height))
|
||||
return account.network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: isAnimated ? "application/x-tgsticker": "image/png", attributes: attributes, stickers: nil, ttlSeconds: nil)))
|
||||
|> mapError { _ -> UploadStickerError in return .generic }
|
||||
|> mapToSignal { media -> Signal<UploadStickerStatus, UploadStickerError> in
|
||||
switch media {
|
||||
case let .messageMediaDocument(_, document, _):
|
||||
if let document = document, let file = telegramMediaFileFromApiDocument(document) {
|
||||
return .single(.complete(file))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return .fail(.generic)
|
||||
}
|
||||
default:
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum CreateStickerSetError {
|
||||
case generic
|
||||
}
|
||||
|
||||
|
||||
public struct ImportSticker {
|
||||
let resource: MediaResource
|
||||
let emojis: [String]
|
||||
let dimensions: PixelDimensions
|
||||
|
||||
public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions) {
|
||||
self.resource = resource
|
||||
self.emojis = emojis
|
||||
self.dimensions = dimensions
|
||||
}
|
||||
}
|
||||
|
||||
public enum CreateStickerSetStatus {
|
||||
case progress(Float)
|
||||
case complete(StickerPackCollectionInfo, [ItemCollectionItem])
|
||||
}
|
||||
|
||||
public func createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, isAnimated: Bool) -> Signal<CreateStickerSetStatus, CreateStickerSetError> {
|
||||
return account.postbox.loadedPeerWithId(account.peerId)
|
||||
|> castError(CreateStickerSetError.self)
|
||||
|> mapToSignal { peer -> Signal<CreateStickerSetStatus, CreateStickerSetError> in
|
||||
guard let inputUser = apiInputUser(peer) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
var uploadStickers: [Signal<UploadStickerStatus, CreateStickerSetError>] = []
|
||||
var stickers = stickers
|
||||
if let thumbnail = thumbnail {
|
||||
stickers.append(thumbnail)
|
||||
}
|
||||
for sticker in stickers {
|
||||
uploadStickers.append(uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, isAnimated: isAnimated)
|
||||
|> mapError { _ -> CreateStickerSetError in
|
||||
return .generic
|
||||
})
|
||||
}
|
||||
return combineLatest(uploadStickers)
|
||||
|> mapToSignal { uploadedStickers -> Signal<CreateStickerSetStatus, CreateStickerSetError> in
|
||||
var documents: [TelegramMediaFile] = []
|
||||
for sticker in uploadedStickers {
|
||||
if case let .complete(document) = sticker {
|
||||
documents.append(document)
|
||||
}
|
||||
}
|
||||
if documents.count == stickers.count {
|
||||
var flags: Int32 = 0
|
||||
if isAnimated {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
var inputStickers: [Api.InputStickerSetItem] = []
|
||||
let stickerDocuments = thumbnail != nil ? documents.dropLast() : documents
|
||||
for i in 0 ..< stickerDocuments.count {
|
||||
let sticker = stickers[i]
|
||||
let document = documents[i]
|
||||
if let resource = document.resource as? CloudDocumentMediaResource {
|
||||
inputStickers.append(.inputStickerSetItem(flags: 0, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.first ?? "", maskCoords: nil))
|
||||
}
|
||||
}
|
||||
var thumbnailDocument: Api.InputDocument?
|
||||
if thumbnail != nil, let document = documents.last, let resource = document.resource as? CloudDocumentMediaResource {
|
||||
flags |= (1 << 2)
|
||||
thumbnailDocument = .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data()))
|
||||
}
|
||||
return account.network.request(Api.functions.stickers.createStickerSet(flags: flags, userId: inputUser, title: title, shortName: shortName, thumb: thumbnailDocument, stickers: inputStickers))
|
||||
|> mapError { error -> CreateStickerSetError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<CreateStickerSetStatus, CreateStickerSetError> in
|
||||
let info: StickerPackCollectionInfo
|
||||
var items: [ItemCollectionItem] = []
|
||||
|
||||
switch result {
|
||||
case let .stickerSet(set, packs, documents):
|
||||
let namespace: ItemCollectionId.Namespace
|
||||
switch set {
|
||||
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _):
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
namespace = Namespaces.ItemCollection.CloudMaskPacks
|
||||
} else {
|
||||
namespace = Namespaces.ItemCollection.CloudStickerPacks
|
||||
}
|
||||
}
|
||||
info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
|
||||
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
|
||||
for pack in packs {
|
||||
switch pack {
|
||||
case let .stickerPack(text, fileIds):
|
||||
let key = ValueBoxKey(text).toMemoryBuffer()
|
||||
for fileId in fileIds {
|
||||
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
||||
if indexKeysByFile[mediaId] == nil {
|
||||
indexKeysByFile[mediaId] = [key]
|
||||
} else {
|
||||
indexKeysByFile[mediaId]!.append(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for apiDocument in documents {
|
||||
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
|
||||
let fileIndexKeys: [MemoryBuffer]
|
||||
if let indexKeys = indexKeysByFile[id] {
|
||||
fileIndexKeys = indexKeys
|
||||
} else {
|
||||
fileIndexKeys = []
|
||||
}
|
||||
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(.complete(info, items))
|
||||
}
|
||||
} else {
|
||||
var totalProgress: Float = 0.0
|
||||
for sticker in uploadedStickers {
|
||||
switch sticker {
|
||||
case .complete:
|
||||
totalProgress += 1.0
|
||||
case let .progress(progress):
|
||||
totalProgress += progress
|
||||
}
|
||||
}
|
||||
let normalizedProgress = min(1.0, max(0.0, totalProgress / Float(stickers.count)))
|
||||
return .single(.progress(normalizedProgress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
BIN
submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/CameraOff.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Call/VideoUnavailable.imageset/CameraOff.pdf
vendored
Normal file
Binary file not shown.
@ -1,6 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "CameraOff.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
Binary file not shown.
@ -6498,7 +6498,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start()
|
||||
|
||||
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, controllerInteraction: strongSelf.controllerInteraction, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sendButtonFrame: node.view.convert(node.bounds, to: nil), textInputNode: textInputNode, completion: { [weak self] in
|
||||
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, controllerInteraction: strongSelf.controllerInteraction, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.supportedOrientations = previousSupportedOrientations
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ final class ChatSendMessageActionSheetController: ViewController {
|
||||
private let controllerInteraction: ChatControllerInteraction?
|
||||
private let interfaceState: ChatPresentationInterfaceState
|
||||
private let gesture: ContextGesture
|
||||
private let sendButtonFrame: CGRect
|
||||
private let sourceSendButton: ASDisplayNode
|
||||
private let textInputNode: EditableTextNode
|
||||
private let completion: () -> Void
|
||||
|
||||
@ -29,12 +29,12 @@ final class ChatSendMessageActionSheetController: ViewController {
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
init(context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sendButtonFrame: CGRect, textInputNode: EditableTextNode, completion: @escaping () -> Void) {
|
||||
init(context: AccountContext, controllerInteraction: ChatControllerInteraction?, interfaceState: ChatPresentationInterfaceState, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, completion: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interfaceState = interfaceState
|
||||
self.gesture = gesture
|
||||
self.sendButtonFrame = sendButtonFrame
|
||||
self.sourceSendButton = sourceSendButton
|
||||
self.textInputNode = textInputNode
|
||||
self.completion = completion
|
||||
|
||||
@ -76,7 +76,7 @@ final class ChatSendMessageActionSheetController: ViewController {
|
||||
canSchedule = !isSecret
|
||||
}
|
||||
|
||||
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, gesture: gesture, sendButtonFrame: self.sendButtonFrame, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in
|
||||
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, forwardedCount: forwardedCount, send: { [weak self] in
|
||||
self?.controllerInteraction?.sendCurrentMessage(false)
|
||||
self?.dismiss(cancel: false)
|
||||
}, sendSilently: { [weak self] in
|
||||
|
@ -154,7 +154,7 @@ private final class ActionSheetItemNode: ASDisplayNode {
|
||||
final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private let sendButtonFrame: CGRect
|
||||
private let sourceSendButton: ASDisplayNode
|
||||
private let textFieldFrame: CGRect
|
||||
private let textInputNode: EditableTextNode
|
||||
private let accessoryPanelNode: AccessoryPanelNode?
|
||||
@ -163,9 +163,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
private let send: (() -> Void)?
|
||||
private let cancel: (() -> Void)?
|
||||
|
||||
private let textCoverNode: ASDisplayNode
|
||||
private let buttonCoverNode: ASDisplayNode
|
||||
|
||||
private let effectView: UIVisualEffectView
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
@ -181,10 +178,14 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(context: AccountContext, reminders: Bool, gesture: ContextGesture, sendButtonFrame: CGRect, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) {
|
||||
private var sendButtonFrame: CGRect {
|
||||
return self.sourceSendButton.view.convert(self.sourceSendButton.bounds, to: nil)
|
||||
}
|
||||
|
||||
init(context: AccountContext, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, forwardedCount: Int?, send: (() -> Void)?, sendSilently: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.sendButtonFrame = sendButtonFrame
|
||||
self.sourceSendButton = sourceSendButton
|
||||
self.textFieldFrame = textInputNode.convert(textInputNode.bounds, to: nil)
|
||||
self.textInputNode = textInputNode
|
||||
self.accessoryPanelNode = nil
|
||||
@ -192,10 +193,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
|
||||
self.send = send
|
||||
self.cancel = cancel
|
||||
|
||||
self.textCoverNode = ASDisplayNode()
|
||||
self.buttonCoverNode = ASDisplayNode()
|
||||
|
||||
|
||||
self.effectView = UIVisualEffectView()
|
||||
if #available(iOS 9.0, *) {
|
||||
} else {
|
||||
@ -249,13 +247,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.contentNodes = contentNodes
|
||||
|
||||
super.init()
|
||||
|
||||
self.textCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.inputBackgroundColor
|
||||
self.addSubnode(self.textCoverNode)
|
||||
|
||||
self.buttonCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor
|
||||
self.addSubnode(self.buttonCoverNode)
|
||||
|
||||
|
||||
self.sendButtonNode.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.presentationData.theme), for: [])
|
||||
self.sendButtonNode.addTarget(self, action: #selector(sendButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
@ -370,8 +362,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor
|
||||
|
||||
self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor
|
||||
self.textCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.inputBackgroundColor
|
||||
self.buttonCoverNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor
|
||||
self.sendButtonNode.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.presentationData.theme), for: [])
|
||||
|
||||
if let toAttributedText = self.textInputNode.attributedText?.mutableCopy() as? NSMutableAttributedString {
|
||||
@ -406,6 +396,9 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.fromMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.toMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false)
|
||||
|
||||
self.textInputNode.isHidden = true
|
||||
self.sourceSendButton.isHidden = true
|
||||
|
||||
if let layout = self.validLayout {
|
||||
let duration = 0.4
|
||||
|
||||
@ -466,8 +459,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
|
||||
let intermediateCompletion: () -> Void = { [weak self] in
|
||||
if completedEffect && completedButton && completedBubble && completedAlpha {
|
||||
self?.textCoverNode.isHidden = true
|
||||
self?.buttonCoverNode.isHidden = true
|
||||
self?.textInputNode.isHidden = false
|
||||
self?.sourceSendButton.isHidden = false
|
||||
completion()
|
||||
}
|
||||
}
|
||||
@ -494,7 +487,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
intermediateCompletion()
|
||||
})
|
||||
} else {
|
||||
self.textCoverNode.isHidden = true
|
||||
self.textInputNode.isHidden = false
|
||||
self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
completedAlpha = true
|
||||
intermediateCompletion()
|
||||
@ -510,7 +503,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
})
|
||||
|
||||
if !cancel {
|
||||
self.buttonCoverNode.isHidden = true
|
||||
self.sourceSendButton.isHidden = false
|
||||
self.sendButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
|
||||
self.sendButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
|
||||
}
|
||||
@ -565,10 +558,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.textCoverNode, frame: self.textFieldFrame)
|
||||
transition.updateFrame(node: self.buttonCoverNode, frame: self.sendButtonFrame.offsetBy(dx: 1.0, dy: 1.0))
|
||||
|
||||
|
||||
transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
|
@ -481,7 +481,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
}
|
||||
case .importStickers:
|
||||
dismissInput()
|
||||
if let navigationController = navigationController, let data = UIPasteboard.general.data(forPasteboardType: "org.telegram.third-party.stickerpack"), let stickerPack = ImportStickerPack(data: data) {
|
||||
if let navigationController = navigationController, let data = UIPasteboard.general.data(forPasteboardType: "org.telegram.third-party.stickerpack"), let stickerPack = ImportStickerPack(data: data), !stickerPack.stickers.isEmpty {
|
||||
for controller in navigationController.overlayControllers {
|
||||
if controller is ImportStickerPackController {
|
||||
controller.dismiss()
|
||||
|
@ -61,7 +61,7 @@ private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode {
|
||||
|
||||
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), title: item.title, text: item.text, style: .plain, linkAction: { link in
|
||||
let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), title: item.title, text: item.text, style: .blocks, linkAction: { link in
|
||||
item.linkAction?(link)
|
||||
}, closeAction: nil)
|
||||
let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0)
|
||||
|
@ -951,14 +951,14 @@ func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKe
|
||||
var hiddenWhileExpanded = false
|
||||
if isOpenedFromChat {
|
||||
switch buttonKey {
|
||||
case .message, .search, .videoCall, .addMember:
|
||||
case .message, .search, .videoCall, .addMember, .leave, .discussion:
|
||||
hiddenWhileExpanded = true
|
||||
default:
|
||||
hiddenWhileExpanded = false
|
||||
}
|
||||
} else {
|
||||
switch buttonKey {
|
||||
case .search, .call, .videoCall, .addMember:
|
||||
case .search, .call, .videoCall, .addMember, .leave, .discussion:
|
||||
hiddenWhileExpanded = true
|
||||
default:
|
||||
hiddenWhileExpanded = false
|
||||
@ -1011,7 +1011,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
if channel.flags.contains(.hasVoiceChat) {
|
||||
hasVoiceChat = true
|
||||
}
|
||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
|
||||
if channel.flags.contains(.isCreator) {
|
||||
displayMore = true
|
||||
}
|
||||
switch channel.info {
|
||||
@ -1118,6 +1118,9 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
}
|
||||
if isExpanded && result.count > 3 {
|
||||
result = result.filter { !peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: $0, isOpenedFromChat: isOpenedFromChat) }
|
||||
if !result.contains(.more) {
|
||||
result.append(.more)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -3687,7 +3687,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self?.openDeletePeer()
|
||||
})))
|
||||
} else {
|
||||
if !headerButtons.contains(.leave) {
|
||||
if filteredButtons.contains(.leave) {
|
||||
if case .member = channel.participationStatus {
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
|
Loading…
x
Reference in New Issue
Block a user