Various improvements

This commit is contained in:
Ilya Laktyushin 2023-03-22 01:07:24 +04:00
parent 9844fd0eb4
commit 32ff62bcb5
17 changed files with 422 additions and 121 deletions

Binary file not shown.

View File

@ -9090,3 +9090,5 @@ Sorry for the inconvenience.";
"PeerInfo.CancelSelectionAlertNo" = "No";
"StickerPacksSettings.SuggestAnimatedEmojiInfo" = "Each time you enter an emoji you can replace it with an animated emoji.";
"DialogList.DeleteBotClearHistory" = "Clear Chat History";

View File

@ -252,6 +252,9 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
}
@objc func nextPressed() {
guard self.confirmationController == nil else {
return
}
let (_, _, number) = self.controllerNode.codeAndNumber
if !number.isEmpty {
let logInNumber = cleanPhoneNumber(self.controllerNode.currentNumber, removePlus: true)

View File

@ -759,6 +759,7 @@ final class PhoneConfirmationController: ViewController {
private let codeTargetNode: ImmediateTextNode
private let phoneTargetNode: ImmediateTextNode
private let measureTargetNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let textActivateAreaNode: AccessibilityAreaNode
@ -824,6 +825,10 @@ final class PhoneConfirmationController: ViewController {
self.phoneTargetNode = ImmediateTextNode()
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)
targetString.addAttribute(NSAttributedString.Key.kern, value: 1.6, range: NSRange(location: 0, length: sourceString.length))
self.phoneTargetNode.attributedText = targetString
@ -1015,6 +1020,12 @@ final class PhoneConfirmationController: ViewController {
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])
self.codeTargetNode.attributedText = NSAttributedString(string: self.code, font: largeFont, textColor: self.theme.list.itemPrimaryTextColor)

View File

@ -3385,7 +3385,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
} else if case let .user(user) = chatPeer, user.botInfo != nil {
canStop = !user.flags.contains(.isSupport)
canClear = user.botInfo == nil
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
} else if case .secretChat = chatPeer {
canClear = true
@ -3450,6 +3449,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} else {
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 {
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
guard let strongSelf = self else {
@ -3495,7 +3510,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}), 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()
guard let strongSelf = self else {
@ -3558,7 +3573,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
})
}))
} else {
} else if !canStop {
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
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),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in

View File

@ -82,6 +82,7 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
public private(set) var isReady: Bool = false
public var isReadyUpdated: (() -> Void)?
public var controllerRemoved: (ViewController) -> Void
public var requestFilterController: (ViewController) -> Void = { _ in }
public var keyboardViewManager: KeyboardViewManager? {
didSet {
}
@ -118,6 +119,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
var statusBarStyle: StatusBarStyle = .Ignore
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
private var panRecognizer: InteractiveTransitionGestureRecognizer?
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 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
}

View File

@ -826,6 +826,9 @@ open class NavigationController: UINavigationController, ContainableController,
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
flatContainer.requestFilterController = { [weak self] controller in
self?.filterController(controller, animated: true)
}
flatContainer.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
@ -853,6 +856,9 @@ open class NavigationController: UINavigationController, ContainableController,
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
flatContainer.requestFilterController = { [weak self] controller in
self?.filterController(controller, animated: true)
}
flatContainer.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self else {
return

View File

@ -3,20 +3,28 @@ import UIKit
import Photos
import SwiftSignalKit
private let imageManager = PHCachingImageManager()
private let imageManager: PHCachingImageManager = {
let imageManager = PHCachingImageManager()
imageManager.allowsCachingHighQualityImages = false
return imageManager
}()
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]
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
let options = PHImageRequestOptions()
options.deliveryMode = deliveryMode
if exact {
options.resizeMode = .exact
}
options.isSynchronous = synchronous
let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
var degraded = false
@ -31,17 +39,15 @@ func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool) -> Signal<UIIma
if let image = image {
subscriber.putNext(image)
if !degraded {
if !degraded || deliveryMode == .fastFormat {
subscriber.putCompletion()
}
}
}
return ActionDisposable {
assetsQueue.async {
imageManager.cancelImageRequest(token)
}
imageManager.cancelImageRequest(token)
}
} |> runOn(assetsQueue)
}
}
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 options = PHVideoRequestOptions()
let token = imageManager.requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
if let avAsset = avAsset {
subscriber.putNext(avAsset)

View File

@ -154,15 +154,20 @@ final class MediaPickerGridItemNode: GridItemNode {
var _cachedTag: Int32?
var tag: Int32? {
// if let tag = self._cachedTag {
// return tag
// } else if let asset = self.asset, let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
// let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
// self._cachedTag = tag
// return tag
// } else {
if let tag = self._cachedTag {
return tag
} else if let (fetchResult, index) = self.currentState {
let asset = fetchResult.object(at: index)
if let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
self._cachedTag = tag
return tag
} else {
return nil
}
} else {
return nil
// }
}
}
func updateSelectionState(animated: Bool = false) {
@ -226,58 +231,6 @@ final class MediaPickerGridItemNode: GridItemNode {
self.backgroundColor = theme.list.mediaPlaceholderColor
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.setNeedsLayout()
}
@ -319,10 +272,17 @@ final class MediaPickerGridItemNode: GridItemNode {
return EmptyDisposable
}
}
let scale = min(2.0, UIScreenScale)
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
|> mapToSignal { result in
if let result = result {

View File

@ -196,6 +196,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
private var placeholderNode: MediaPickerPlaceholderNode?
private var manageNode: MediaPickerManageNode?
private var scrollingArea: SparseItemGridScrollingArea
private var isFastScrolling = false
private var selectionNode: MediaPickerSelectedListNode?
@ -213,12 +214,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
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 let _ready = Promise<Bool>()
var ready: Promise<Bool> {
return self._ready
}
fileprivate var isSuspended = false
private var hasGallery = false
private var isCameraPreviewVisible = true
private var validLayout: (ContainerViewLayout, CGFloat)?
init(controller: MediaPickerScreen) {
@ -248,7 +258,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.gridNode)
//self.containerNode.addSubnode(self.scrollingArea)
self.containerNode.addSubnode(self.scrollingArea)
let preloadPromise = self.preloadPromise
let updatedState: Signal<State, NoError>
@ -357,9 +367,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.hiddenMediaDisposable?.dispose()
self.selectionChangedDisposable?.dispose()
self.itemsDimensionsUpdatedDisposable?.dispose()
self.fastScrollDisposable?.dispose()
}
private var selectionGesture: MediaPickerGridSelectionGesture<TGMediaSelectableItem>?
override func didLoad() {
super.didLoad()
@ -402,16 +412,44 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
guard let strongSelf = self else {
return nil
}
strongSelf.controller?.requestAttachmentMenuExpansion()
strongSelf.isFastScrolling = true
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
guard let strongSelf = self else {
return
}
// strongSelf.isFastScrolling = true
strongSelf.gridNode.scrollView.setContentOffset(offset, animated: false)
// strongSelf.isFastScrolling = false
Queue.concurrentDefaultQueue().async {
strongSelf.fastScrollContentOffset.set(offset)
}
}
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 {
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 {
if otherGestureRecognizer.view is UIScrollView || otherGestureRecognizer is UIPanGestureRecognizer {
return true
@ -706,9 +753,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.controller?.present(c, in: .window(.root), with: a)
}, finishedTransitionIn: { [weak self] in
self?.openingMedia = false
self?.cameraView?.pausePreview()
self?.hasGallery = true
self?.updateIsCameraActive()
}, willTransitionOut: { [weak self] in
self?.cameraView?.resumePreview()
self?.hasGallery = false
self?.updateIsCameraActive()
}, dismissAll: { [weak self] in
self?.controller?.dismissAll()
})
@ -742,9 +791,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true)
}, finishedTransitionIn: { [weak self] in
self?.openingMedia = false
self?.cameraView?.pausePreview()
self?.hasGallery = true
self?.updateIsCameraActive()
}, willTransitionOut: { [weak self] in
self?.cameraView?.resumePreview()
self?.hasGallery = false
self?.updateIsCameraActive()
}, dismissAll: { [weak self] in
self?.controller?.dismissAll()
})
@ -1590,12 +1641,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
self.scrollToTop?()
self.controllerNode.cameraView?.pausePreview()
self.controllerNode.isSuspended = true
self.controllerNode.updateIsCameraActive()
}
public func prepareForReuse() {
self.controllerNode.cameraView?.resumePreview()
self.controllerNode.isSuspended = false
self.controllerNode.updateIsCameraActive()
self.controllerNode.updateNavigation(delayDisappear: true, transition: .immediate)
}

View File

@ -1350,6 +1350,12 @@ public final class SparseItemGrid: ASDisplayNode {
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) {
self.theme = theme

View File

@ -948,6 +948,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
private var activityTimer: SwiftSignalKit.Timer?
public var beginScrolling: (() -> UIScrollView?)?
public var finishedScrolling: (() -> Void)?
public var setContentOffset: ((CGPoint) -> Void)?
public var openCurrentDate: (() -> Void)?
@ -1059,6 +1060,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
strongSelf.updateLineIndicator(transition: transition)
strongSelf.updateActivityTimer(isScrolling: false)
strongSelf.finishedScrolling?()
},
moved: { [weak self] relativeOffset in
guard let strongSelf = self else {

View File

@ -104,7 +104,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
components.append(.isHidden(hidden == .boolTrue))
}
return TelegramMediaAction(action: .topicEdited(components: components))
case let.messageActionSuggestProfilePhoto(photo):
case let .messageActionSuggestProfilePhoto(photo):
return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo)))
case let .messageActionRequestedPeer(buttonId, peer):
return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerId: peer.peerId))

View File

@ -2447,7 +2447,152 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
}
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 {
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)
}
}
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)
}
}
}

View File

@ -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.maskNode.isForum = isForum
self.pinchSourceNode.update(size: size, transition: transition)
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)
@ -2579,7 +2580,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.presentationData = presentationData
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let credibilityIcon: CredibilityIcon
if let peer = peer {
if peer.isFake {
@ -2599,6 +2599,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
credibilityIcon = .none
}
var isForum = false
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
isForum = true
}
if themeUpdated || 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)
}
} 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
transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame
@ -3207,12 +3216,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
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
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.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 {
@ -3305,7 +3309,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
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.bottomCoverNode.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 {
var frameIndex: Int = 0
var isForum = false {
didSet {
if self.isForum != oldValue {
self.update(frameIndex: self.frameIndex)
}
}
}
func update(_ value: CGFloat) {
let lowerBound = 0
let upperBound = 180
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
if frameIndex != self.frameIndex {
self.frameIndex = frameIndex
self.trackTo(item: ManagedAnimationItem(source: .local("UserAvatarMask"), frames: .range(startFrame: frameIndex, endFrame: frameIndex), duration: 0.001))
self.update(frameIndex: frameIndex)
}
}
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 {

View File

@ -374,6 +374,9 @@ final class SharedMediaPlayer {
strongSelf.forceAudioToSpeaker = forceAudioToSpeaker
strongSelf.playbackItem?.setForceAudioToSpeaker(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))
} else {
strongSelf.control(.playback(.pause))

View File

@ -122,6 +122,9 @@ public final class WebSearchController: ViewController {
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) {
self.context = context
self.mode = mode
@ -195,7 +198,7 @@ public final class WebSearchController: ViewController {
self.navigationContentNode = navigationContentNode
navigationContentNode.setQueryUpdated { [weak self] query in
if let strongSelf = self, strongSelf.isNodeLoaded {
strongSelf.updateSearchQuery(query)
strongSelf.searchQueryPromise.set(query)
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) {
@ -298,6 +319,7 @@ public final class WebSearchController: ViewController {
self.disposable?.dispose()
self.resultsDisposable.dispose()
self.selectionDisposable?.dispose()
self.searchQueryDisposable?.dispose()
}
public func cancel() {