mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
9844fd0eb4
commit
32ff62bcb5
BIN
Telegram/Telegram-iOS/Resources/ForumAvatarMask.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/ForumAvatarMask.tgs
Normal file
Binary file not shown.
@ -9090,3 +9090,5 @@ Sorry for the inconvenience.";
|
|||||||
"PeerInfo.CancelSelectionAlertNo" = "No";
|
"PeerInfo.CancelSelectionAlertNo" = "No";
|
||||||
|
|
||||||
"StickerPacksSettings.SuggestAnimatedEmojiInfo" = "Each time you enter an emoji you can replace it with an animated emoji.";
|
"StickerPacksSettings.SuggestAnimatedEmojiInfo" = "Each time you enter an emoji you can replace it with an animated emoji.";
|
||||||
|
|
||||||
|
"DialogList.DeleteBotClearHistory" = "Clear Chat History";
|
||||||
|
@ -252,6 +252,9 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func nextPressed() {
|
@objc func nextPressed() {
|
||||||
|
guard self.confirmationController == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
let (_, _, number) = self.controllerNode.codeAndNumber
|
let (_, _, number) = self.controllerNode.codeAndNumber
|
||||||
if !number.isEmpty {
|
if !number.isEmpty {
|
||||||
let logInNumber = cleanPhoneNumber(self.controllerNode.currentNumber, removePlus: true)
|
let logInNumber = cleanPhoneNumber(self.controllerNode.currentNumber, removePlus: true)
|
||||||
|
@ -759,6 +759,7 @@ final class PhoneConfirmationController: ViewController {
|
|||||||
|
|
||||||
private let codeTargetNode: ImmediateTextNode
|
private let codeTargetNode: ImmediateTextNode
|
||||||
private let phoneTargetNode: ImmediateTextNode
|
private let phoneTargetNode: ImmediateTextNode
|
||||||
|
private let measureTargetNode: ImmediateTextNode
|
||||||
|
|
||||||
private let textNode: ImmediateTextNode
|
private let textNode: ImmediateTextNode
|
||||||
private let textActivateAreaNode: AccessibilityAreaNode
|
private let textActivateAreaNode: AccessibilityAreaNode
|
||||||
@ -824,6 +825,10 @@ final class PhoneConfirmationController: ViewController {
|
|||||||
self.phoneTargetNode = ImmediateTextNode()
|
self.phoneTargetNode = ImmediateTextNode()
|
||||||
self.phoneTargetNode.displaysAsynchronously = false
|
self.phoneTargetNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
self.measureTargetNode = ImmediateTextNode()
|
||||||
|
self.measureTargetNode.displaysAsynchronously = false
|
||||||
|
self.measureTargetNode.maximumNumberOfLines = 1
|
||||||
|
|
||||||
let targetString = NSMutableAttributedString(string: number, font: largeFont, textColor: theme.list.itemPrimaryTextColor)
|
let targetString = NSMutableAttributedString(string: number, font: largeFont, textColor: theme.list.itemPrimaryTextColor)
|
||||||
targetString.addAttribute(NSAttributedString.Key.kern, value: 1.6, range: NSRange(location: 0, length: sourceString.length))
|
targetString.addAttribute(NSAttributedString.Key.kern, value: 1.6, range: NSRange(location: 0, length: sourceString.length))
|
||||||
self.phoneTargetNode.attributedText = targetString
|
self.phoneTargetNode.attributedText = targetString
|
||||||
@ -1015,6 +1020,12 @@ final class PhoneConfirmationController: ViewController {
|
|||||||
fontSize = 30.0
|
fontSize = 30.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.measureTargetNode.attributedText = NSAttributedString(string: self.code + " " + self.number, font: Font.with(size: fontSize, design: .regular, weight: .bold, traits: [.monospacedNumbers]), textColor: self.theme.list.itemPrimaryTextColor)
|
||||||
|
let measuredSize = self.measureTargetNode.updateLayout(CGSize(width: 1000.0, height: .greatestFiniteMagnitude))
|
||||||
|
if measuredSize.width > maxWidth {
|
||||||
|
fontSize = floor(0.8 * fontSize)
|
||||||
|
}
|
||||||
|
|
||||||
let largeFont = Font.with(size: fontSize, design: .regular, weight: .bold, traits: [.monospacedNumbers])
|
let largeFont = Font.with(size: fontSize, design: .regular, weight: .bold, traits: [.monospacedNumbers])
|
||||||
|
|
||||||
self.codeTargetNode.attributedText = NSAttributedString(string: self.code, font: largeFont, textColor: self.theme.list.itemPrimaryTextColor)
|
self.codeTargetNode.attributedText = NSAttributedString(string: self.code, font: largeFont, textColor: self.theme.list.itemPrimaryTextColor)
|
||||||
|
@ -3385,7 +3385,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
} else if case let .user(user) = chatPeer, user.botInfo != nil {
|
} else if case let .user(user) = chatPeer, user.botInfo != nil {
|
||||||
canStop = !user.flags.contains(.isSupport)
|
canStop = !user.flags.contains(.isSupport)
|
||||||
canClear = user.botInfo == nil
|
|
||||||
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
|
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
|
||||||
} else if case .secretChat = chatPeer {
|
} else if case .secretChat = chatPeer {
|
||||||
canClear = true
|
canClear = true
|
||||||
@ -3450,6 +3449,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
} else {
|
} else {
|
||||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||||
|
|
||||||
|
if canStop {
|
||||||
|
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
|
||||||
|
actionSheet?.dismissAnimated()
|
||||||
|
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in
|
||||||
|
}, removed: {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.peerId, isBlocked: true).start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if canClear {
|
if canClear {
|
||||||
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
|
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -3495,7 +3510,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}), in: .current)
|
}), in: .current)
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
|
items.append(ActionSheetButtonItem(title: canStop ? strongSelf.presentationData.strings.DialogList_DeleteBotClearHistory : strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
|
||||||
actionSheet?.dismissAnimated()
|
actionSheet?.dismissAnimated()
|
||||||
|
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -3558,7 +3573,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
strongSelf.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
strongSelf.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
} else {
|
} else if !canStop {
|
||||||
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
|
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
|
||||||
actionSheet?.dismissAnimated()
|
actionSheet?.dismissAnimated()
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -3630,23 +3645,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if canStop {
|
|
||||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
|
|
||||||
actionSheet?.dismissAnimated()
|
|
||||||
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in
|
|
||||||
}, removed: {
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.peerId, isBlocked: true).start()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
|
||||||
ActionSheetItemGroup(items: [
|
ActionSheetItemGroup(items: [
|
||||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||||
|
@ -82,6 +82,7 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
public private(set) var isReady: Bool = false
|
public private(set) var isReady: Bool = false
|
||||||
public var isReadyUpdated: (() -> Void)?
|
public var isReadyUpdated: (() -> Void)?
|
||||||
public var controllerRemoved: (ViewController) -> Void
|
public var controllerRemoved: (ViewController) -> Void
|
||||||
|
public var requestFilterController: (ViewController) -> Void = { _ in }
|
||||||
public var keyboardViewManager: KeyboardViewManager? {
|
public var keyboardViewManager: KeyboardViewManager? {
|
||||||
didSet {
|
didSet {
|
||||||
}
|
}
|
||||||
@ -118,6 +119,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
var statusBarStyle: StatusBarStyle = .Ignore
|
var statusBarStyle: StatusBarStyle = .Ignore
|
||||||
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
||||||
|
|
||||||
public init(isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
public init(isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
||||||
@ -211,7 +214,10 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
let topController = self.controllers[self.controllers.count - 1]
|
let topController = self.controllers[self.controllers.count - 1]
|
||||||
let bottomController = self.controllers[self.controllers.count - 2]
|
let bottomController = self.controllers[self.controllers.count - 2]
|
||||||
|
|
||||||
if !topController.attemptNavigation({
|
if !topController.attemptNavigation({ [weak self, weak topController] in
|
||||||
|
if let self, let topController {
|
||||||
|
self.requestFilterController(topController)
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -826,6 +826,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
|
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
|
||||||
self?.controllerRemoved(controller)
|
self?.controllerRemoved(controller)
|
||||||
})
|
})
|
||||||
|
flatContainer.requestFilterController = { [weak self] controller in
|
||||||
|
self?.filterController(controller, animated: true)
|
||||||
|
}
|
||||||
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -853,6 +856,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
|
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
|
||||||
self?.controllerRemoved(controller)
|
self?.controllerRemoved(controller)
|
||||||
})
|
})
|
||||||
|
flatContainer.requestFilterController = { [weak self] controller in
|
||||||
|
self?.filterController(controller, animated: true)
|
||||||
|
}
|
||||||
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
|
@ -3,20 +3,28 @@ import UIKit
|
|||||||
import Photos
|
import Photos
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
private let imageManager = PHCachingImageManager()
|
private let imageManager: PHCachingImageManager = {
|
||||||
|
let imageManager = PHCachingImageManager()
|
||||||
|
imageManager.allowsCachingHighQualityImages = false
|
||||||
|
return imageManager
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
private let assetsQueue = Queue()
|
private let assetsQueue = Queue()
|
||||||
|
|
||||||
func assetImage(fetchResult: PHFetchResult<PHAsset>, index: Int, targetSize: CGSize, exact: Bool) -> Signal<UIImage?, NoError> {
|
func assetImage(fetchResult: PHFetchResult<PHAsset>, index: Int, targetSize: CGSize, exact: Bool, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic, synchronous: Bool = false) -> Signal<UIImage?, NoError> {
|
||||||
let asset = fetchResult[index]
|
let asset = fetchResult[index]
|
||||||
return assetImage(asset: asset, targetSize: targetSize, exact: exact)
|
return assetImage(asset: asset, targetSize: targetSize, exact: exact, deliveryMode: deliveryMode, synchronous: synchronous)
|
||||||
}
|
}
|
||||||
|
|
||||||
func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool) -> Signal<UIImage?, NoError> {
|
func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic, synchronous: Bool = false) -> Signal<UIImage?, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let options = PHImageRequestOptions()
|
let options = PHImageRequestOptions()
|
||||||
|
options.deliveryMode = deliveryMode
|
||||||
if exact {
|
if exact {
|
||||||
options.resizeMode = .exact
|
options.resizeMode = .exact
|
||||||
}
|
}
|
||||||
|
options.isSynchronous = synchronous
|
||||||
let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
|
let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
|
||||||
var degraded = false
|
var degraded = false
|
||||||
|
|
||||||
@ -31,17 +39,15 @@ func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool) -> Signal<UIIma
|
|||||||
|
|
||||||
if let image = image {
|
if let image = image {
|
||||||
subscriber.putNext(image)
|
subscriber.putNext(image)
|
||||||
if !degraded {
|
if !degraded || deliveryMode == .fastFormat {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
assetsQueue.async {
|
imageManager.cancelImageRequest(token)
|
||||||
imageManager.cancelImageRequest(token)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} |> runOn(assetsQueue)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAsset?, NoError> {
|
func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAsset?, NoError> {
|
||||||
@ -49,7 +55,6 @@ func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAss
|
|||||||
let asset = fetchResult[index]
|
let asset = fetchResult[index]
|
||||||
|
|
||||||
let options = PHVideoRequestOptions()
|
let options = PHVideoRequestOptions()
|
||||||
|
|
||||||
let token = imageManager.requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
|
let token = imageManager.requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
|
||||||
if let avAsset = avAsset {
|
if let avAsset = avAsset {
|
||||||
subscriber.putNext(avAsset)
|
subscriber.putNext(avAsset)
|
||||||
|
@ -154,15 +154,20 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||||||
|
|
||||||
var _cachedTag: Int32?
|
var _cachedTag: Int32?
|
||||||
var tag: Int32? {
|
var tag: Int32? {
|
||||||
// if let tag = self._cachedTag {
|
if let tag = self._cachedTag {
|
||||||
// return tag
|
return tag
|
||||||
// } else if let asset = self.asset, let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
|
} else if let (fetchResult, index) = self.currentState {
|
||||||
// let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
|
let asset = fetchResult.object(at: index)
|
||||||
// self._cachedTag = tag
|
if let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
|
||||||
// return tag
|
let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
|
||||||
// } else {
|
self._cachedTag = tag
|
||||||
|
return tag
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return nil
|
return nil
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSelectionState(animated: Bool = false) {
|
func updateSelectionState(animated: Bool = false) {
|
||||||
@ -226,58 +231,6 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||||||
self.backgroundColor = theme.list.mediaPlaceholderColor
|
self.backgroundColor = theme.list.mediaPlaceholderColor
|
||||||
|
|
||||||
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentState!.1 != index {
|
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentState!.1 != index {
|
||||||
// let editingContext = interaction.editingState
|
|
||||||
// let asset = media.asset as? TGMediaEditableItem
|
|
||||||
//
|
|
||||||
// let editedSignal = Signal<UIImage?, NoError> { subscriber in
|
|
||||||
// if let signal = editingContext.thumbnailImageSignal(forIdentifier: media.identifier) {
|
|
||||||
// let disposable = signal.start(next: { next in
|
|
||||||
// if let image = next as? UIImage {
|
|
||||||
// subscriber.putNext(image)
|
|
||||||
// } else {
|
|
||||||
// subscriber.putNext(nil)
|
|
||||||
// }
|
|
||||||
// }, error: { _ in
|
|
||||||
// }, completed: nil)!
|
|
||||||
//
|
|
||||||
// return ActionDisposable {
|
|
||||||
// disposable.dispose()
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// return EmptyDisposable
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let originalImageSignal = Signal<UIImage?, NoError> { subscriber in
|
|
||||||
// if let signal = asset?.thumbnailImageSignal?()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let scale = min(2.0, UIScreenScale)
|
|
||||||
// let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
|
||||||
// let originalSignal: Signal<UIImage, NoError> = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
|
|
||||||
// let imageSignal: Signal<UIImage?, NoError> = editedSignal
|
|
||||||
// |> mapToSignal { result in
|
|
||||||
// if let result = result {
|
|
||||||
// return .single(result)
|
|
||||||
// } else {
|
|
||||||
// return originalSignal
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// self.imageNode.setSignal(imageSignal)
|
|
||||||
//
|
|
||||||
// if case .video = media, let asset = media.asset as? TGCameraCapturedVideo {
|
|
||||||
// self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
|
|
||||||
//
|
|
||||||
// if self.typeIconNode.supernode == nil {
|
|
||||||
// self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.videoDuration)), font: Font.semibold(12.0), textColor: .white)
|
|
||||||
//
|
|
||||||
// self.addSubnode(self.gradientNode)
|
|
||||||
// self.addSubnode(self.typeIconNode)
|
|
||||||
// self.addSubnode(self.durationNode)
|
|
||||||
// self.setNeedsLayout()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
self.currentMediaState = (media.asset, index)
|
self.currentMediaState = (media.asset, index)
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
@ -319,10 +272,17 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scale = min(2.0, UIScreenScale)
|
let scale = min(2.0, UIScreenScale)
|
||||||
let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||||
let originalSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
|
|
||||||
|
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|
||||||
|
|> then(
|
||||||
|
assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|
||||||
|
|> delay(0.03, queue: Queue.concurrentDefaultQueue())
|
||||||
|
)
|
||||||
|
|
||||||
|
let originalSignal = assetImageSignal //assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, synchronous: true)
|
||||||
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|
||||||
|> mapToSignal { result in
|
|> mapToSignal { result in
|
||||||
if let result = result {
|
if let result = result {
|
||||||
|
@ -196,6 +196,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
private var placeholderNode: MediaPickerPlaceholderNode?
|
private var placeholderNode: MediaPickerPlaceholderNode?
|
||||||
private var manageNode: MediaPickerManageNode?
|
private var manageNode: MediaPickerManageNode?
|
||||||
private var scrollingArea: SparseItemGridScrollingArea
|
private var scrollingArea: SparseItemGridScrollingArea
|
||||||
|
private var isFastScrolling = false
|
||||||
|
|
||||||
private var selectionNode: MediaPickerSelectedListNode?
|
private var selectionNode: MediaPickerSelectedListNode?
|
||||||
|
|
||||||
@ -213,12 +214,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
|
|
||||||
private let hiddenMediaId = Promise<String?>(nil)
|
private let hiddenMediaId = Promise<String?>(nil)
|
||||||
|
|
||||||
|
private var selectionGesture: MediaPickerGridSelectionGesture<TGMediaSelectableItem>?
|
||||||
|
|
||||||
|
private var fastScrollContentOffset = ValuePromise<CGPoint>(ignoreRepeated: true)
|
||||||
|
private var fastScrollDisposable: Disposable?
|
||||||
|
|
||||||
private var didSetReady = false
|
private var didSetReady = false
|
||||||
private let _ready = Promise<Bool>()
|
private let _ready = Promise<Bool>()
|
||||||
var ready: Promise<Bool> {
|
var ready: Promise<Bool> {
|
||||||
return self._ready
|
return self._ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate var isSuspended = false
|
||||||
|
private var hasGallery = false
|
||||||
|
private var isCameraPreviewVisible = true
|
||||||
|
|
||||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||||
|
|
||||||
init(controller: MediaPickerScreen) {
|
init(controller: MediaPickerScreen) {
|
||||||
@ -248,7 +258,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.addSubnode(self.containerNode)
|
self.addSubnode(self.containerNode)
|
||||||
self.containerNode.addSubnode(self.backgroundNode)
|
self.containerNode.addSubnode(self.backgroundNode)
|
||||||
self.containerNode.addSubnode(self.gridNode)
|
self.containerNode.addSubnode(self.gridNode)
|
||||||
//self.containerNode.addSubnode(self.scrollingArea)
|
self.containerNode.addSubnode(self.scrollingArea)
|
||||||
|
|
||||||
let preloadPromise = self.preloadPromise
|
let preloadPromise = self.preloadPromise
|
||||||
let updatedState: Signal<State, NoError>
|
let updatedState: Signal<State, NoError>
|
||||||
@ -357,9 +367,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.hiddenMediaDisposable?.dispose()
|
self.hiddenMediaDisposable?.dispose()
|
||||||
self.selectionChangedDisposable?.dispose()
|
self.selectionChangedDisposable?.dispose()
|
||||||
self.itemsDimensionsUpdatedDisposable?.dispose()
|
self.itemsDimensionsUpdatedDisposable?.dispose()
|
||||||
|
self.fastScrollDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectionGesture: MediaPickerGridSelectionGesture<TGMediaSelectableItem>?
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
@ -402,16 +412,44 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
strongSelf.controller?.requestAttachmentMenuExpansion()
|
||||||
|
strongSelf.isFastScrolling = true
|
||||||
return strongSelf.gridNode.scrollView
|
return strongSelf.gridNode.scrollView
|
||||||
}
|
}
|
||||||
|
self.scrollingArea.finishedScrolling = { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.isFastScrolling = false
|
||||||
|
}
|
||||||
self.scrollingArea.setContentOffset = { [weak self] offset in
|
self.scrollingArea.setContentOffset = { [weak self] offset in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// strongSelf.isFastScrolling = true
|
Queue.concurrentDefaultQueue().async {
|
||||||
strongSelf.gridNode.scrollView.setContentOffset(offset, animated: false)
|
strongSelf.fastScrollContentOffset.set(offset)
|
||||||
// strongSelf.isFastScrolling = false
|
}
|
||||||
}
|
}
|
||||||
|
self.gridNode.visibleItemsUpdated = { [weak self] _ in
|
||||||
|
self?.updateScrollingArea()
|
||||||
|
|
||||||
|
if let self, let cameraView = self.cameraView {
|
||||||
|
self.isCameraPreviewVisible = self.gridNode.scrollView.bounds.intersects(cameraView.frame)
|
||||||
|
self.updateIsCameraActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.updateScrollingArea()
|
||||||
|
|
||||||
|
let throttledContentOffsetSignal = self.fastScrollContentOffset.get()
|
||||||
|
|> mapToThrottled { next -> Signal<CGPoint, NoError> in
|
||||||
|
return .single(next) |> then(.complete() |> delay(0.02, queue: Queue.concurrentDefaultQueue()))
|
||||||
|
}
|
||||||
|
self.fastScrollDisposable = (throttledContentOffsetSignal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] contentOffset in
|
||||||
|
if let self {
|
||||||
|
self.gridNode.scrollView.setContentOffset(contentOffset, animated: false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if let controller = self.controller, case .assets(nil) = controller.subject {
|
if let controller = self.controller, case .assets(nil) = controller.subject {
|
||||||
let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true
|
let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true
|
||||||
@ -442,6 +480,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateIsCameraActive() {
|
||||||
|
let isCameraActive = !self.isSuspended && !self.hasGallery && self.isCameraPreviewVisible
|
||||||
|
if isCameraActive {
|
||||||
|
self.cameraView?.resumePreview()
|
||||||
|
} else {
|
||||||
|
self.cameraView?.pausePreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
if otherGestureRecognizer.view is UIScrollView || otherGestureRecognizer is UIPanGestureRecognizer {
|
if otherGestureRecognizer.view is UIScrollView || otherGestureRecognizer is UIPanGestureRecognizer {
|
||||||
return true
|
return true
|
||||||
@ -706,9 +753,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self?.controller?.present(c, in: .window(.root), with: a)
|
self?.controller?.present(c, in: .window(.root), with: a)
|
||||||
}, finishedTransitionIn: { [weak self] in
|
}, finishedTransitionIn: { [weak self] in
|
||||||
self?.openingMedia = false
|
self?.openingMedia = false
|
||||||
self?.cameraView?.pausePreview()
|
self?.hasGallery = true
|
||||||
|
self?.updateIsCameraActive()
|
||||||
}, willTransitionOut: { [weak self] in
|
}, willTransitionOut: { [weak self] in
|
||||||
self?.cameraView?.resumePreview()
|
self?.hasGallery = false
|
||||||
|
self?.updateIsCameraActive()
|
||||||
}, dismissAll: { [weak self] in
|
}, dismissAll: { [weak self] in
|
||||||
self?.controller?.dismissAll()
|
self?.controller?.dismissAll()
|
||||||
})
|
})
|
||||||
@ -742,9 +791,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||||||
}, finishedTransitionIn: { [weak self] in
|
}, finishedTransitionIn: { [weak self] in
|
||||||
self?.openingMedia = false
|
self?.openingMedia = false
|
||||||
self?.cameraView?.pausePreview()
|
self?.hasGallery = true
|
||||||
|
self?.updateIsCameraActive()
|
||||||
}, willTransitionOut: { [weak self] in
|
}, willTransitionOut: { [weak self] in
|
||||||
self?.cameraView?.resumePreview()
|
self?.hasGallery = false
|
||||||
|
self?.updateIsCameraActive()
|
||||||
}, dismissAll: { [weak self] in
|
}, dismissAll: { [weak self] in
|
||||||
self?.controller?.dismissAll()
|
self?.controller?.dismissAll()
|
||||||
})
|
})
|
||||||
@ -1590,12 +1641,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
self.scrollToTop?()
|
self.scrollToTop?()
|
||||||
|
|
||||||
self.controllerNode.cameraView?.pausePreview()
|
self.controllerNode.isSuspended = true
|
||||||
|
self.controllerNode.updateIsCameraActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func prepareForReuse() {
|
public func prepareForReuse() {
|
||||||
self.controllerNode.cameraView?.resumePreview()
|
self.controllerNode.isSuspended = false
|
||||||
|
self.controllerNode.updateIsCameraActive()
|
||||||
self.controllerNode.updateNavigation(delayDisappear: true, transition: .immediate)
|
self.controllerNode.updateNavigation(delayDisappear: true, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1350,6 +1350,12 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
self.currentViewport?.scrollView.isScrollEnabled = self.isScrollEnabled
|
self.currentViewport?.scrollView.isScrollEnabled = self.isScrollEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func scrollWithDelta(_ delta: CGFloat) {
|
||||||
|
if let scrollView = self.currentViewport?.scrollView {
|
||||||
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentOffset.y + delta), animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public init(theme: PresentationTheme, initialZoomLevel: ZoomLevel? = nil) {
|
public init(theme: PresentationTheme, initialZoomLevel: ZoomLevel? = nil) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
@ -948,6 +948,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
private var activityTimer: SwiftSignalKit.Timer?
|
private var activityTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
public var beginScrolling: (() -> UIScrollView?)?
|
public var beginScrolling: (() -> UIScrollView?)?
|
||||||
|
public var finishedScrolling: (() -> Void)?
|
||||||
public var setContentOffset: ((CGPoint) -> Void)?
|
public var setContentOffset: ((CGPoint) -> Void)?
|
||||||
public var openCurrentDate: (() -> Void)?
|
public var openCurrentDate: (() -> Void)?
|
||||||
|
|
||||||
@ -1059,6 +1060,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
strongSelf.updateLineIndicator(transition: transition)
|
strongSelf.updateLineIndicator(transition: transition)
|
||||||
|
|
||||||
strongSelf.updateActivityTimer(isScrolling: false)
|
strongSelf.updateActivityTimer(isScrolling: false)
|
||||||
|
|
||||||
|
strongSelf.finishedScrolling?()
|
||||||
},
|
},
|
||||||
moved: { [weak self] relativeOffset in
|
moved: { [weak self] relativeOffset in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
@ -104,7 +104,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
|||||||
components.append(.isHidden(hidden == .boolTrue))
|
components.append(.isHidden(hidden == .boolTrue))
|
||||||
}
|
}
|
||||||
return TelegramMediaAction(action: .topicEdited(components: components))
|
return TelegramMediaAction(action: .topicEdited(components: components))
|
||||||
case let.messageActionSuggestProfilePhoto(photo):
|
case let .messageActionSuggestProfilePhoto(photo):
|
||||||
return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo)))
|
return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo)))
|
||||||
case let .messageActionRequestedPeer(buttonId, peer):
|
case let .messageActionRequestedPeer(buttonId, peer):
|
||||||
return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerId: peer.peerId))
|
return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerId: peer.peerId))
|
||||||
|
@ -2447,7 +2447,152 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
||||||
private var listSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
private var listSelectionGesture: MediaListSelectionRecognizer?
|
||||||
|
|
||||||
|
override func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||||
|
selectionRecognizer.shouldBegin = {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
self.view.addGestureRecognizer(selectionRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])?
|
||||||
|
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
|
||||||
|
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
|
||||||
|
private var selectionScrollDelta: CGFloat?
|
||||||
|
private var selectionLastLocation: CGPoint?
|
||||||
|
|
||||||
|
private func messageAtPoint(_ location: CGPoint) -> EngineMessage? {
|
||||||
|
if let itemView = self.itemGrid.item(at: location)?.view as? ItemView, let message = itemView.item?.message {
|
||||||
|
return EngineMessage(message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
|
||||||
|
let location = recognizer.location(in: self.view)
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
if let message = self.messageAtPoint(location) {
|
||||||
|
let selecting = !(self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
|
||||||
|
self.selectionPanState = (selecting, message.id, [])
|
||||||
|
self.chatControllerInteraction.toggleMessagesSelection([message.id], selecting)
|
||||||
|
}
|
||||||
|
case .changed:
|
||||||
|
self.handlePanSelection(location: location)
|
||||||
|
self.selectionLastLocation = location
|
||||||
|
case .ended, .failed, .cancelled:
|
||||||
|
self.selectionPanState = nil
|
||||||
|
self.selectionScrollDisplayLink = nil
|
||||||
|
self.selectionScrollActivationTimer?.invalidate()
|
||||||
|
self.selectionScrollActivationTimer = nil
|
||||||
|
self.selectionScrollDelta = nil
|
||||||
|
self.selectionLastLocation = nil
|
||||||
|
self.selectionScrollSkipUpdate = false
|
||||||
|
case .possible:
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handlePanSelection(location: CGPoint) {
|
||||||
|
var location = location
|
||||||
|
if location.y < 0.0 {
|
||||||
|
location.y = 5.0
|
||||||
|
} else if location.y > self.frame.height {
|
||||||
|
location.y = self.frame.height - 5.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasState = false
|
||||||
|
if let state = self.selectionPanState {
|
||||||
|
hasState = true
|
||||||
|
if let message = self.messageAtPoint(location) {
|
||||||
|
if message.id == state.initialMessageId {
|
||||||
|
if !state.toggledMessageIds.isEmpty {
|
||||||
|
self.chatControllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0.compactMap({ $0 }) }, !state.selecting)
|
||||||
|
self.selectionPanState = (state.selecting, state.initialMessageId, [])
|
||||||
|
}
|
||||||
|
} else if state.toggledMessageIds.last?.first != message.id {
|
||||||
|
var updatedToggledMessageIds: [[EngineMessage.Id]] = []
|
||||||
|
var previouslyToggled = false
|
||||||
|
for i in (0 ..< state.toggledMessageIds.count) {
|
||||||
|
if let messageId = state.toggledMessageIds[i].first {
|
||||||
|
if messageId == message.id {
|
||||||
|
previouslyToggled = true
|
||||||
|
updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1))
|
||||||
|
|
||||||
|
let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 }
|
||||||
|
self.chatControllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !previouslyToggled {
|
||||||
|
updatedToggledMessageIds = state.toggledMessageIds
|
||||||
|
let isSelected = self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false
|
||||||
|
if state.selecting != isSelected {
|
||||||
|
updatedToggledMessageIds.append([message.id])
|
||||||
|
self.chatControllerInteraction.toggleMessagesSelection([message.id], state.selecting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard hasState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let scrollingAreaHeight: CGFloat = 50.0
|
||||||
|
if location.y < scrollingAreaHeight || location.y > self.frame.height - scrollingAreaHeight {
|
||||||
|
if location.y < self.frame.height / 2.0 {
|
||||||
|
self.selectionScrollDelta = (scrollingAreaHeight - location.y) / scrollingAreaHeight
|
||||||
|
} else {
|
||||||
|
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - location.y)))) / scrollingAreaHeight
|
||||||
|
}
|
||||||
|
if let displayLink = self.selectionScrollDisplayLink {
|
||||||
|
displayLink.isPaused = false
|
||||||
|
} else {
|
||||||
|
if let _ = self.selectionScrollActivationTimer {
|
||||||
|
} else {
|
||||||
|
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
|
||||||
|
self?.setupSelectionScrolling()
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
timer.start()
|
||||||
|
self.selectionScrollActivationTimer = timer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.selectionScrollDisplayLink?.isPaused = true
|
||||||
|
self.selectionScrollActivationTimer?.invalidate()
|
||||||
|
self.selectionScrollActivationTimer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectionScrollSkipUpdate = false
|
||||||
|
private func setupSelectionScrolling() {
|
||||||
|
self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||||
|
self?.selectionScrollActivationTimer = nil
|
||||||
|
if let strongSelf = self, let delta = strongSelf.selectionScrollDelta {
|
||||||
|
let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta))
|
||||||
|
let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down
|
||||||
|
let _ = strongSelf.itemGrid.scrollWithDelta(direction == .up ? -distance : distance)
|
||||||
|
|
||||||
|
if let location = strongSelf.selectionLastLocation {
|
||||||
|
if !strongSelf.selectionScrollSkipUpdate {
|
||||||
|
strongSelf.handlePanSelection(location: location)
|
||||||
|
}
|
||||||
|
strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.selectionScrollDisplayLink?.isPaused = false
|
||||||
|
}
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||||
@ -2717,3 +2862,65 @@ func updateVisualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messag
|
|||||||
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.visualMediaStoredState, id: key)
|
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.visualMediaStoredState, id: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MediaListSelectionRecognizer: UIPanGestureRecognizer {
|
||||||
|
private let selectionGestureActivationThreshold: CGFloat = 5.0
|
||||||
|
|
||||||
|
var recognized: Bool? = nil
|
||||||
|
var initialLocation: CGPoint = CGPoint()
|
||||||
|
|
||||||
|
public var shouldBegin: (() -> Bool)?
|
||||||
|
|
||||||
|
public override init(target: Any?, action: Selector?) {
|
||||||
|
super.init(target: target, action: action)
|
||||||
|
|
||||||
|
self.minimumNumberOfTouches = 2
|
||||||
|
self.maximumNumberOfTouches = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func reset() {
|
||||||
|
super.reset()
|
||||||
|
|
||||||
|
self.recognized = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
|
if let shouldBegin = self.shouldBegin, !shouldBegin() {
|
||||||
|
self.state = .failed
|
||||||
|
} else {
|
||||||
|
let touch = touches.first!
|
||||||
|
self.initialLocation = touch.location(in: self.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
let location = touches.first!.location(in: self.view)
|
||||||
|
let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y)
|
||||||
|
|
||||||
|
let touchesArray = Array(touches)
|
||||||
|
if self.recognized == nil, touchesArray.count == 2 {
|
||||||
|
if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last {
|
||||||
|
let firstLocation = firstTouch.location(in: self.view)
|
||||||
|
let secondLocation = secondTouch.location(in: self.view)
|
||||||
|
|
||||||
|
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
|
||||||
|
let dx = v1.x - v2.x
|
||||||
|
let dy = v1.y - v2.y
|
||||||
|
return sqrt(dx * dx + dy * dy)
|
||||||
|
}
|
||||||
|
if distance(firstLocation, secondLocation) > 200.0 {
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.state != .failed && (abs(translation.y) >= selectionGestureActivationThreshold) {
|
||||||
|
self.recognized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let recognized = self.recognized, recognized {
|
||||||
|
super.touchesMoved(touches, with: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1125,8 +1125,9 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
|
||||||
self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded)
|
self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded)
|
||||||
|
self.maskNode.isForum = isForum
|
||||||
self.pinchSourceNode.update(size: size, transition: transition)
|
self.pinchSourceNode.update(size: size, transition: transition)
|
||||||
self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size)
|
self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings)
|
self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings)
|
||||||
@ -2579,7 +2580,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
|
|
||||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
|
||||||
|
|
||||||
let credibilityIcon: CredibilityIcon
|
let credibilityIcon: CredibilityIcon
|
||||||
if let peer = peer {
|
if let peer = peer {
|
||||||
if peer.isFake {
|
if peer.isFake {
|
||||||
@ -2599,6 +2599,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
credibilityIcon = .none
|
credibilityIcon = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isForum = false
|
||||||
|
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||||
|
isForum = true
|
||||||
|
}
|
||||||
|
|
||||||
if themeUpdated || self.currentCredibilityIcon != credibilityIcon {
|
if themeUpdated || self.currentCredibilityIcon != credibilityIcon {
|
||||||
self.currentCredibilityIcon = credibilityIcon
|
self.currentCredibilityIcon = credibilityIcon
|
||||||
|
|
||||||
@ -2742,7 +2747,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view)
|
transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transitionSourceAvatarFrame = avatarFrame.offsetBy(dx: 0.0, dy: -avatarFrame.maxY).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
|
if deviceMetrics.hasDynamicIsland {
|
||||||
|
transitionSourceAvatarFrame = CGRect(origin: CGPoint(x: avatarFrame.minX, y: -20.0), size: avatarFrame.size).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
|
||||||
|
} else {
|
||||||
|
transitionSourceAvatarFrame = avatarFrame.offsetBy(dx: 0.0, dy: -avatarFrame.maxY).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
transitionSourceTitleFrame = navigationTransition.sourceTitleFrame
|
transitionSourceTitleFrame = navigationTransition.sourceTitleFrame
|
||||||
transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame
|
transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame
|
||||||
@ -3207,12 +3216,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
transition.updateAlpha(node: subtitleArrowNode, alpha: (1.0 - titleCollapseFraction))
|
transition.updateAlpha(node: subtitleArrowNode, alpha: (1.0 - titleCollapseFraction))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isForum = false
|
|
||||||
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
|
||||||
isForum = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let avatarCornerRadius: CGFloat = isForum ? floor(avatarSize * 0.25) : avatarSize / 2.0
|
let avatarCornerRadius: CGFloat = isForum ? floor(avatarSize * 0.25) : avatarSize / 2.0
|
||||||
|
|
||||||
if self.isAvatarExpanded {
|
if self.isAvatarExpanded {
|
||||||
@ -3240,7 +3244,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition)
|
self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, isForum: isForum, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition)
|
||||||
self.editingContentNode.avatarNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
|
self.editingContentNode.avatarNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
|
||||||
self.avatarOverlayNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
|
self.avatarOverlayNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
|
||||||
if additive {
|
if additive {
|
||||||
@ -3305,7 +3309,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale)
|
transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale)
|
||||||
}
|
}
|
||||||
|
|
||||||
if deviceMetrics.hasDynamicIsland && !isForum && self.forumTopicThreadId == nil {
|
if deviceMetrics.hasDynamicIsland && self.forumTopicThreadId == nil {
|
||||||
self.avatarListNode.maskNode.frame = CGRect(origin: CGPoint(x: -85.5, y: -self.avatarListNode.frame.minY + 48.0), size: CGSize(width: 171.0, height: 171.0))
|
self.avatarListNode.maskNode.frame = CGRect(origin: CGPoint(x: -85.5, y: -self.avatarListNode.frame.minY + 48.0), size: CGSize(width: 171.0, height: 171.0))
|
||||||
self.avatarListNode.bottomCoverNode.frame = self.avatarListNode.maskNode.frame
|
self.avatarListNode.bottomCoverNode.frame = self.avatarListNode.maskNode.frame
|
||||||
self.avatarListNode.topCoverNode.frame = self.avatarListNode.maskNode.frame
|
self.avatarListNode.topCoverNode.frame = self.avatarListNode.maskNode.frame
|
||||||
@ -3677,15 +3681,27 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
private class DynamicIslandMaskNode: ManagedAnimationNode {
|
private class DynamicIslandMaskNode: ManagedAnimationNode {
|
||||||
var frameIndex: Int = 0
|
var frameIndex: Int = 0
|
||||||
|
|
||||||
|
var isForum = false {
|
||||||
|
didSet {
|
||||||
|
if self.isForum != oldValue {
|
||||||
|
self.update(frameIndex: self.frameIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func update(_ value: CGFloat) {
|
func update(_ value: CGFloat) {
|
||||||
let lowerBound = 0
|
let lowerBound = 0
|
||||||
let upperBound = 180
|
let upperBound = 180
|
||||||
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
||||||
if frameIndex != self.frameIndex {
|
if frameIndex != self.frameIndex {
|
||||||
self.frameIndex = frameIndex
|
self.update(frameIndex: frameIndex)
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("UserAvatarMask"), frames: .range(startFrame: frameIndex, endFrame: frameIndex), duration: 0.001))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(frameIndex: Int) {
|
||||||
|
self.frameIndex = frameIndex
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local(self.isForum ? "ForumAvatarMask" : "UserAvatarMask"), frames: .range(startFrame: frameIndex, endFrame: frameIndex), duration: 0.001))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DynamicIslandBlurNode: ASDisplayNode {
|
private class DynamicIslandBlurNode: ASDisplayNode {
|
||||||
|
@ -374,6 +374,9 @@ final class SharedMediaPlayer {
|
|||||||
strongSelf.forceAudioToSpeaker = forceAudioToSpeaker
|
strongSelf.forceAudioToSpeaker = forceAudioToSpeaker
|
||||||
strongSelf.playbackItem?.setForceAudioToSpeaker(forceAudioToSpeaker)
|
strongSelf.playbackItem?.setForceAudioToSpeaker(forceAudioToSpeaker)
|
||||||
if !forceAudioToSpeaker {
|
if !forceAudioToSpeaker {
|
||||||
|
if let playbackStateValue = strongSelf._playbackStateValue, case let .item(item) = playbackStateValue, item.status.timestamp < 1.5 {
|
||||||
|
strongSelf.control(.seek(0.0))
|
||||||
|
}
|
||||||
strongSelf.control(.playback(.play))
|
strongSelf.control(.playback(.play))
|
||||||
} else {
|
} else {
|
||||||
strongSelf.control(.playback(.pause))
|
strongSelf.control(.playback(.pause))
|
||||||
|
@ -122,6 +122,9 @@ public final class WebSearchController: ViewController {
|
|||||||
|
|
||||||
public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true }
|
public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true }
|
||||||
|
|
||||||
|
private var searchQueryPromise = ValuePromise<String>()
|
||||||
|
private var searchQueryDisposable: Disposable?
|
||||||
|
|
||||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode, activateOnDisplay: Bool = true) {
|
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode, activateOnDisplay: Bool = true) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
@ -195,7 +198,7 @@ public final class WebSearchController: ViewController {
|
|||||||
self.navigationContentNode = navigationContentNode
|
self.navigationContentNode = navigationContentNode
|
||||||
navigationContentNode.setQueryUpdated { [weak self] query in
|
navigationContentNode.setQueryUpdated { [weak self] query in
|
||||||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||||||
strongSelf.updateSearchQuery(query)
|
strongSelf.searchQueryPromise.set(query)
|
||||||
strongSelf.searchingUpdated(!query.isEmpty)
|
strongSelf.searchingUpdated(!query.isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,6 +291,24 @@ public final class WebSearchController: ViewController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let throttledSearchQuery = self.searchQueryPromise.get()
|
||||||
|
|> mapToSignal { query -> Signal<String, NoError> in
|
||||||
|
if !query.isEmpty {
|
||||||
|
return (.complete() |> delay(0.6, queue: Queue.mainQueue()))
|
||||||
|
|> then(.single(query))
|
||||||
|
} else {
|
||||||
|
return .single(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.searchQueryDisposable = (throttledSearchQuery
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] query in
|
||||||
|
if let self {
|
||||||
|
self.updateSearchQuery(query)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(coder aDecoder: NSCoder) {
|
required public init(coder aDecoder: NSCoder) {
|
||||||
@ -298,6 +319,7 @@ public final class WebSearchController: ViewController {
|
|||||||
self.disposable?.dispose()
|
self.disposable?.dispose()
|
||||||
self.resultsDisposable.dispose()
|
self.resultsDisposable.dispose()
|
||||||
self.selectionDisposable?.dispose()
|
self.selectionDisposable?.dispose()
|
||||||
|
self.searchQueryDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func cancel() {
|
public func cancel() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user