mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Various Improvements
This commit is contained in:
parent
a260717c88
commit
8d58a2b239
@ -1,12 +1,44 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import LegacyComponents
|
||||
|
||||
public final class VoiceBlobNode: ASDisplayNode {
|
||||
public init(
|
||||
maxLevel: CGFloat,
|
||||
smallBlobRange: VoiceBlobView.BlobRange,
|
||||
mediumBlobRange: VoiceBlobView.BlobRange,
|
||||
bigBlobRange: VoiceBlobView.BlobRange
|
||||
) {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return VoiceBlobView(frame: CGRect(), maxLevel: maxLevel, smallBlobRange: smallBlobRange, mediumBlobRange: mediumBlobRange, bigBlobRange: bigBlobRange)
|
||||
})
|
||||
}
|
||||
|
||||
public func startAnimating(immediately: Bool = false) {
|
||||
(self.view as? VoiceBlobView)?.startAnimating(immediately: immediately)
|
||||
}
|
||||
|
||||
public func stopAnimating(duration: Double = 0.15) {
|
||||
(self.view as? VoiceBlobView)?.stopAnimating(duration: duration)
|
||||
}
|
||||
|
||||
public func setColor(_ color: UIColor, animated: Bool = false) {
|
||||
(self.view as? VoiceBlobView)?.setColor(color, animated: animated)
|
||||
}
|
||||
|
||||
public func updateLevel(_ level: CGFloat, immediately: Bool = false) {
|
||||
(self.view as? VoiceBlobView)?.updateLevel(level, immediately: immediately)
|
||||
}
|
||||
}
|
||||
|
||||
public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDecoration {
|
||||
private let smallBlob: BlobView
|
||||
private let mediumBlob: BlobView
|
||||
private let bigBlob: BlobView
|
||||
private let smallBlob: BlobNode
|
||||
private let mediumBlob: BlobNode
|
||||
private let bigBlob: BlobNode
|
||||
|
||||
private let maxLevel: CGFloat
|
||||
|
||||
@ -28,7 +60,7 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
||||
) {
|
||||
self.maxLevel = maxLevel
|
||||
|
||||
self.smallBlob = BlobView(
|
||||
self.smallBlob = BlobNode(
|
||||
pointsCount: 8,
|
||||
minRandomness: 0.1,
|
||||
maxRandomness: 0.5,
|
||||
@ -39,23 +71,23 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: true
|
||||
)
|
||||
self.mediumBlob = BlobView(
|
||||
self.mediumBlob = BlobNode(
|
||||
pointsCount: 8,
|
||||
minRandomness: 1,
|
||||
maxRandomness: 1,
|
||||
minSpeed: 1.5,
|
||||
maxSpeed: 7,
|
||||
minSpeed: 0.9,
|
||||
maxSpeed: 4,
|
||||
minScale: mediumBlobRange.min,
|
||||
maxScale: mediumBlobRange.max,
|
||||
scaleSpeed: 0.2,
|
||||
isCircle: false
|
||||
)
|
||||
self.bigBlob = BlobView(
|
||||
self.bigBlob = BlobNode(
|
||||
pointsCount: 8,
|
||||
minRandomness: 1,
|
||||
maxRandomness: 1,
|
||||
minSpeed: 1.5,
|
||||
maxSpeed: 7,
|
||||
minSpeed: 0.9,
|
||||
maxSpeed: 4,
|
||||
minScale: bigBlobRange.min,
|
||||
maxScale: bigBlobRange.max,
|
||||
scaleSpeed: 0.2,
|
||||
@ -64,9 +96,9 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(bigBlob)
|
||||
addSubview(mediumBlob)
|
||||
addSubview(smallBlob)
|
||||
self.addSubnode(self.bigBlob)
|
||||
self.addSubnode(self.mediumBlob)
|
||||
self.addSubnode(self.smallBlob)
|
||||
|
||||
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
@ -148,8 +180,8 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
||||
}
|
||||
|
||||
private func updateBlobsState() {
|
||||
if isAnimating {
|
||||
if smallBlob.frame.size != .zero {
|
||||
if self.isAnimating {
|
||||
if self.smallBlob.frame.size != .zero {
|
||||
smallBlob.startAnimating()
|
||||
mediumBlob.startAnimating()
|
||||
bigBlob.startAnimating()
|
||||
@ -164,15 +196,15 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
smallBlob.frame = bounds
|
||||
mediumBlob.frame = bounds
|
||||
bigBlob.frame = bounds
|
||||
self.smallBlob.frame = bounds
|
||||
self.mediumBlob.frame = bounds
|
||||
self.bigBlob.frame = bounds
|
||||
|
||||
updateBlobsState()
|
||||
self.updateBlobsState()
|
||||
}
|
||||
}
|
||||
|
||||
final class BlobView: UIView {
|
||||
final class BlobNode: ASDisplayNode {
|
||||
let pointsCount: Int
|
||||
let smoothness: CGFloat
|
||||
|
||||
@ -186,8 +218,6 @@ final class BlobView: UIView {
|
||||
let maxScale: CGFloat
|
||||
let scaleSpeed: CGFloat
|
||||
|
||||
var scaleLevelsToBalance = [CGFloat]()
|
||||
|
||||
let isCircle: Bool
|
||||
|
||||
var level: CGFloat = 0 {
|
||||
@ -261,9 +291,9 @@ final class BlobView: UIView {
|
||||
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
||||
|
||||
super.init(frame: .zero)
|
||||
super.init()
|
||||
|
||||
layer.addSublayer(shapeLayer)
|
||||
self.layer.addSublayer(self.shapeLayer)
|
||||
|
||||
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||
}
|
||||
@ -282,10 +312,6 @@ final class BlobView: UIView {
|
||||
|
||||
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||
self.speedLevel = max(self.speedLevel, newSpeedLevel)
|
||||
|
||||
// if abs(lastSpeedLevel - newSpeedLevel) > 0.5 {
|
||||
// animateToNewShape()
|
||||
// }
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
@ -368,16 +394,16 @@ final class BlobView: UIView {
|
||||
return points
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
if isCircle {
|
||||
let halfWidth = bounds.width * 0.5
|
||||
shapeLayer.path = UIBezierPath(
|
||||
roundedRect: bounds.offsetBy(dx: -halfWidth, dy: -halfWidth),
|
||||
self.shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
if self.isCircle {
|
||||
let halfWidth = self.bounds.width * 0.5
|
||||
self.shapeLayer.path = UIBezierPath(
|
||||
roundedRect: self.bounds.offsetBy(dx: -halfWidth, dy: -halfWidth),
|
||||
cornerRadius: halfWidth
|
||||
).cgPath
|
||||
}
|
||||
@ -386,7 +412,6 @@ final class BlobView: UIView {
|
||||
}
|
||||
|
||||
private extension UIBezierPath {
|
||||
|
||||
static func smoothCurve(
|
||||
through points: [CGPoint],
|
||||
length: CGFloat,
|
||||
@ -439,7 +464,6 @@ private extension UIBezierPath {
|
||||
}
|
||||
|
||||
struct SmoothPoint {
|
||||
|
||||
let point: CGPoint
|
||||
|
||||
let inAngle: CGFloat
|
||||
@ -464,4 +488,3 @@ private extension UIBezierPath {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1272,6 +1272,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
return
|
||||
}
|
||||
if !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing {
|
||||
var isEditing = false
|
||||
strongSelf.chatListDisplayNode.containerNode.updateState { state in
|
||||
isEditing = state.editing
|
||||
return state
|
||||
}
|
||||
if !isEditing {
|
||||
strongSelf.editPressed()
|
||||
}
|
||||
strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing = true
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
|
@ -182,7 +182,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
||||
let peer1 = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt32Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
|
||||
let timestamp1: Int32 = 100000
|
||||
let peers = SimpleDictionary<PeerId, Peer>()
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in })
|
||||
|
@ -1217,6 +1217,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
self?.listNode.clearHighlightAnimated(true)
|
||||
}, disabledPeerSelected: { _ in
|
||||
}, togglePeerSelected: { _ in
|
||||
}, togglePeersSelection: { _, _ in
|
||||
}, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { [weak self] peer, message, _ in
|
||||
interaction.dismissInput()
|
||||
@ -2324,7 +2325,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
let timestamp1: Int32 = 100000
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
peers[peer1.id] = peer1
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in })
|
||||
|
@ -696,7 +696,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
|
||||
}
|
||||
|
||||
self.contextContainer.isGestureEnabled = enablePreview
|
||||
self.contextContainer.isGestureEnabled = enablePreview && !item.editing
|
||||
}
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
@ -1509,7 +1509,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.onlineIsVoiceChat = onlineIsVoiceChat
|
||||
|
||||
strongSelf.contextContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||
|
||||
|
||||
if case .groupReference = item.content {
|
||||
strongSelf.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, layout.contentSize.height - itemHeight, 0.0)
|
||||
}
|
||||
|
@ -48,10 +48,16 @@ final class ChatListHighlightedLocation {
|
||||
}
|
||||
|
||||
public final class ChatListNodeInteraction {
|
||||
public enum PeerEntry {
|
||||
case peerId(PeerId)
|
||||
case peer(Peer)
|
||||
}
|
||||
|
||||
let activateSearch: () -> Void
|
||||
let peerSelected: (Peer, ChatListNodeEntryPromoInfo?) -> Void
|
||||
let disabledPeerSelected: (Peer) -> Void
|
||||
let togglePeerSelected: (Peer) -> Void
|
||||
let togglePeersSelection: ([PeerEntry], Bool) -> Void
|
||||
let additionalCategorySelected: (Int) -> Void
|
||||
let messageSelected: (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void
|
||||
let groupSelected: (PeerGroupId) -> Void
|
||||
@ -70,11 +76,12 @@ public final class ChatListNodeInteraction {
|
||||
public var searchTextHighightState: String?
|
||||
var highlightedChatLocation: ChatListHighlightedLocation?
|
||||
|
||||
public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (Peer) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId, Bool) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (Peer) -> Void, togglePeersSelection: @escaping ([PeerEntry], Bool) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId, Bool) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
self.activateSearch = activateSearch
|
||||
self.peerSelected = peerSelected
|
||||
self.disabledPeerSelected = disabledPeerSelected
|
||||
self.togglePeerSelected = togglePeerSelected
|
||||
self.togglePeersSelection = togglePeersSelection
|
||||
self.additionalCategorySelected = additionalCategorySelected
|
||||
self.messageSelected = messageSelected
|
||||
self.groupSelected = groupSelected
|
||||
@ -565,6 +572,8 @@ public final class ChatListNode: ListView {
|
||||
var didBeginSelectingChats: (() -> Void)?
|
||||
public var selectionCountChanged: ((Int) -> Void)?
|
||||
|
||||
var isSelectionGestureEnabled = true
|
||||
|
||||
public init(context: AccountContext, groupId: PeerGroupId, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, disableAnimations: Bool) {
|
||||
self.context = context
|
||||
self.groupId = groupId
|
||||
@ -625,6 +634,34 @@ public final class ChatListNode: ListView {
|
||||
if didBeginSelecting {
|
||||
self?.didBeginSelectingChats?()
|
||||
}
|
||||
}, togglePeersSelection: { [weak self] peers, selected in
|
||||
self?.updateState { state in
|
||||
var state = state
|
||||
if selected {
|
||||
for peerEntry in peers {
|
||||
switch peerEntry {
|
||||
case let .peer(peer):
|
||||
state.selectedPeerIds.insert(peer.id)
|
||||
state.selectedPeerMap[peer.id] = peer
|
||||
case let .peerId(peerId):
|
||||
state.selectedPeerIds.insert(peerId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for peerEntry in peers {
|
||||
switch peerEntry {
|
||||
case let .peer(peer):
|
||||
state.selectedPeerIds.remove(peer.id)
|
||||
case let .peerId(peerId):
|
||||
state.selectedPeerIds.remove(peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
if selected && !peers.isEmpty {
|
||||
self?.didBeginSelectingChats?()
|
||||
}
|
||||
}, additionalCategorySelected: { [weak self] id in
|
||||
self?.additionalCategorySelected?(id)
|
||||
}, messageSelected: { [weak self] peer, message, promoInfo in
|
||||
@ -1315,6 +1352,15 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
|
||||
self.resetFilter()
|
||||
|
||||
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||
selectionRecognizer.shouldBegin = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
return strongSelf.isSelectionGestureEnabled
|
||||
}
|
||||
self.view.addGestureRecognizer(selectionRecognizer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -1898,6 +1944,140 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func peerAtPoint(_ point: CGPoint) -> Peer? {
|
||||
var resultPeer: Peer?
|
||||
self.forEachVisibleItemNode { itemNode in
|
||||
if resultPeer == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) {
|
||||
if let itemNode = itemNode as? ChatListItemNode, let item = itemNode.item {
|
||||
switch item.content {
|
||||
case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _):
|
||||
resultPeer = peer.peer
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resultPeer
|
||||
}
|
||||
|
||||
private var selectionPanState: (selecting: Bool, initialPeerId: PeerId, toggledPeerIds: [[PeerId]])?
|
||||
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
|
||||
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
|
||||
private var selectionScrollDelta: CGFloat?
|
||||
private var selectionLastLocation: CGPoint?
|
||||
|
||||
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
|
||||
let location = recognizer.location(in: self.view)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let peer = self.peerAtPoint(location) {
|
||||
let selecting = !self.currentState.selectedPeerIds.contains(peer.id)
|
||||
self.selectionPanState = (selecting, peer.id, [])
|
||||
self.interaction?.togglePeersSelection([.peer(peer)], 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) {
|
||||
if let state = self.selectionPanState {
|
||||
if let peer = self.peerAtPoint(location) {
|
||||
if peer.id == state.initialPeerId {
|
||||
if !state.toggledPeerIds.isEmpty {
|
||||
self.interaction?.togglePeersSelection(state.toggledPeerIds.flatMap { $0.compactMap({ .peerId($0) }) }, !state.selecting)
|
||||
self.selectionPanState = (state.selecting, state.initialPeerId, [])
|
||||
}
|
||||
} else if state.toggledPeerIds.last?.first != peer.id {
|
||||
var updatedToggledPeerIds: [[PeerId]] = []
|
||||
var previouslyToggled = false
|
||||
for i in (0 ..< state.toggledPeerIds.count) {
|
||||
if let peerId = state.toggledPeerIds[i].first {
|
||||
if peerId == peer.id {
|
||||
previouslyToggled = true
|
||||
updatedToggledPeerIds = Array(state.toggledPeerIds.prefix(i + 1))
|
||||
|
||||
let peerIdsToToggle = Array(state.toggledPeerIds.suffix(state.toggledPeerIds.count - i - 1)).flatMap { $0 }
|
||||
self.interaction?.togglePeersSelection(peerIdsToToggle.compactMap { .peerId($0) }, !state.selecting)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !previouslyToggled {
|
||||
updatedToggledPeerIds = state.toggledPeerIds
|
||||
let isSelected = self.currentState.selectedPeerIds.contains(peer.id)
|
||||
if state.selecting != isSelected {
|
||||
updatedToggledPeerIds.append([peer.id])
|
||||
self.interaction?.togglePeersSelection([.peer(peer)], state.selecting)
|
||||
}
|
||||
}
|
||||
|
||||
self.selectionPanState = (state.selecting, state.initialPeerId, updatedToggledPeerIds)
|
||||
}
|
||||
}
|
||||
|
||||
let scrollingAreaHeight: CGFloat = 50.0
|
||||
if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom {
|
||||
if location.y < self.frame.height / 2.0 {
|
||||
self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight
|
||||
} else {
|
||||
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - 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.scrollWithDirection(direction, distance: distance)
|
||||
|
||||
if let location = strongSelf.selectionLastLocation {
|
||||
if !strongSelf.selectionScrollSkipUpdate {
|
||||
strongSelf.handlePanSelection(location: location)
|
||||
}
|
||||
strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate
|
||||
}
|
||||
}
|
||||
})
|
||||
self.selectionScrollDisplayLink?.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
private func statusStringForPeerType(accountPeerId: PeerId, strings: PresentationStrings, peer: Peer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> (String, Bool)? {
|
||||
@ -1951,3 +2131,65 @@ private func statusStringForPeerType(accountPeerId: PeerId, strings: Presentatio
|
||||
}
|
||||
return (strings.ChatList_PeerTypeNonContact, false)
|
||||
}
|
||||
|
||||
public class ChatHistoryListSelectionRecognizer: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, peerSelected: { _, _ in
|
||||
}, disabledPeerSelected: { _ in
|
||||
}, togglePeerSelected: { _ in
|
||||
}, togglePeersSelection: { _, _ in
|
||||
}, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { [weak self] peer, message, _ in
|
||||
if let strongSelf = self {
|
||||
|
@ -1414,7 +1414,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
||||
|
||||
- (CGFloat)_brushWeightForSize:(CGFloat)size
|
||||
{
|
||||
return [self _brushBaseWeightForCurrentPainting] + [self _brushWeightRangeForCurrentPainting] * size;
|
||||
return ([self _brushBaseWeightForCurrentPainting] + [self _brushWeightRangeForCurrentPainting] * size) / _scrollView.zoomScale;
|
||||
}
|
||||
|
||||
+ (CGSize)maximumPaintingSize
|
||||
@ -1739,6 +1739,9 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
||||
{
|
||||
[self adjustZoom];
|
||||
|
||||
TGPaintSwatch *currentSwatch = _portraitSettingsView.swatch;
|
||||
[_canvasView setBrushWeight:[self _brushWeightForSize:currentSwatch.brushWeight]];
|
||||
|
||||
if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON)
|
||||
{
|
||||
[TGHacks setAnimationDurationFactor:0.5f];
|
||||
|
@ -132,6 +132,11 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
private let imageNode: ASImageNode
|
||||
private let displayLink: CADisplayLink
|
||||
|
||||
public var imageUpdated: ((UIImage) -> Void)?
|
||||
public var image: UIImage? {
|
||||
return self.imageNode.image
|
||||
}
|
||||
|
||||
public var state: ManagedAnimationState?
|
||||
public var trackStack: [ManagedAnimationItem] = []
|
||||
public var didTryAdvancingState = false
|
||||
@ -260,6 +265,7 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
} else {
|
||||
self.imageNode.image = image
|
||||
}
|
||||
self.imageUpdated?(image)
|
||||
}
|
||||
|
||||
for (callbackFrame, callback) in state.item.callbacks {
|
||||
|
@ -317,13 +317,17 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
if value != self._statusValue {
|
||||
if let value = value, value.seekId == self.ignoreSeekId {
|
||||
} else {
|
||||
let previousStatusValue = self._statusValue
|
||||
self._statusValue = value
|
||||
self.updateProgressAnimations()
|
||||
|
||||
let playbackStatus = value?.status
|
||||
var playbackStatus = value?.status
|
||||
if self.playbackStatusValue != playbackStatus {
|
||||
self.playbackStatusValue = playbackStatus
|
||||
if let playbackStatusUpdated = self.playbackStatusUpdated {
|
||||
if playbackStatus == .paused, previousStatusValue?.status == .playing, let value = value, value.timestamp > value.duration - 0.1 {
|
||||
playbackStatus = .playing
|
||||
}
|
||||
playbackStatusUpdated(playbackStatus)
|
||||
}
|
||||
}
|
||||
|
@ -362,6 +362,10 @@ final class PasscodeEntryControllerNode: ASDisplayNode {
|
||||
if !iconFrame.isEmpty {
|
||||
self.iconNode.animateIn(fromScale: 0.416)
|
||||
self.iconNode.layer.animatePosition(from: iconFrame.center.offsetBy(dx: 6.0, dy: 6.0), to: self.iconNode.layer.position, duration: 0.45)
|
||||
|
||||
Queue.mainQueue().after(0.45) {
|
||||
self.hapticFeedback.impact(.medium)
|
||||
}
|
||||
}
|
||||
|
||||
self.subtitleNode.isHidden = true
|
||||
|
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -6,6 +6,7 @@ import SwiftSignalKit
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import AppBundle
|
||||
import ManagedAnimationNode
|
||||
|
||||
public enum SemanticStatusNodeState: Equatable {
|
||||
public struct ProgressAppearance: Equatable {
|
||||
@ -41,6 +42,7 @@ private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
|
||||
|
||||
private protocol SemanticStatusNodeStateContext: class {
|
||||
var isAnimating: Bool { get }
|
||||
var requestUpdate: () -> Void { get set }
|
||||
|
||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
|
||||
}
|
||||
@ -90,10 +92,12 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
|
||||
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
|
||||
let transitionFraction: CGFloat
|
||||
let icon: SemanticStatusNodeIcon
|
||||
let iconImage: UIImage?
|
||||
|
||||
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon) {
|
||||
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?) {
|
||||
self.transitionFraction = transitionFraction
|
||||
self.icon = icon
|
||||
self.iconImage = iconImage
|
||||
|
||||
super.init()
|
||||
}
|
||||
@ -119,38 +123,65 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
|
||||
break
|
||||
case .play:
|
||||
let diameter = size.width
|
||||
|
||||
let factor = diameter / 50.0
|
||||
|
||||
let size = CGSize(width: 15.0, height: 18.0)
|
||||
context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0)
|
||||
|
||||
|
||||
let size: CGSize
|
||||
var offset: CGFloat = 0.0
|
||||
if let iconImage = self.iconImage {
|
||||
size = iconImage.size
|
||||
} else {
|
||||
offset = 1.5
|
||||
size = CGSize(width: 15.0, height: 18.0)
|
||||
}
|
||||
context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0)
|
||||
if (diameter < 40.0) {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: factor)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
}
|
||||
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
|
||||
context.fillPath()
|
||||
if let iconImage = self.iconImage {
|
||||
context.saveGState()
|
||||
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
|
||||
context.clip(to: iconRect, mask: iconImage.cgImage!)
|
||||
context.fill(iconRect)
|
||||
context.restoreGState()
|
||||
} else {
|
||||
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
|
||||
context.fillPath()
|
||||
}
|
||||
if (diameter < 40.0) {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
}
|
||||
context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0)
|
||||
context.translateBy(x: -(diameter - size.width) / 2.0 - offset, y: -(diameter - size.height) / 2.0)
|
||||
case .pause:
|
||||
let diameter = size.width
|
||||
|
||||
let factor = diameter / 50.0
|
||||
|
||||
let size = CGSize(width: 15.0, height: 16.0)
|
||||
let size: CGSize
|
||||
if let iconImage = self.iconImage {
|
||||
size = iconImage.size
|
||||
} else {
|
||||
size = CGSize(width: 15.0, height: 16.0)
|
||||
}
|
||||
context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0)
|
||||
if (diameter < 40.0) {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: factor)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
}
|
||||
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
|
||||
context.fillPath()
|
||||
if let iconImage = self.iconImage {
|
||||
context.saveGState()
|
||||
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
|
||||
context.clip(to: iconRect, mask: iconImage.cgImage!)
|
||||
context.fill(iconRect)
|
||||
context.restoreGState()
|
||||
} else {
|
||||
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
|
||||
context.fillPath()
|
||||
}
|
||||
if (diameter < 40.0) {
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
|
||||
@ -159,7 +190,6 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
|
||||
context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0)
|
||||
case let .custom(image):
|
||||
let diameter = size.width
|
||||
|
||||
let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size)
|
||||
|
||||
context.saveGState()
|
||||
@ -210,18 +240,36 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
|
||||
}
|
||||
}
|
||||
|
||||
let icon: SemanticStatusNodeIcon
|
||||
var icon: SemanticStatusNodeIcon {
|
||||
didSet {
|
||||
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil)
|
||||
}
|
||||
}
|
||||
|
||||
var animationNode: PlayPauseIconNode?
|
||||
var iconImage: UIImage?
|
||||
|
||||
init(icon: SemanticStatusNodeIcon) {
|
||||
self.icon = icon
|
||||
|
||||
if [.play, .pause].contains(icon) {
|
||||
self.animationNode = PlayPauseIconNode()
|
||||
self.animationNode?.imageUpdated = { [weak self] image in
|
||||
self?.iconImage = image
|
||||
self?.requestUpdate()
|
||||
}
|
||||
self.iconImage = self.animationNode?.image
|
||||
}
|
||||
}
|
||||
|
||||
var isAnimating: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var requestUpdate: () -> Void = {}
|
||||
|
||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
||||
return DrawingState(transitionFraction: transitionFraction, icon: self.icon)
|
||||
return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage)
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,6 +424,8 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo
|
||||
return true
|
||||
}
|
||||
|
||||
var requestUpdate: () -> Void = {}
|
||||
|
||||
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) {
|
||||
self.value = value
|
||||
self.displayCancel = displayCancel
|
||||
@ -402,6 +452,10 @@ private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateCo
|
||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
|
||||
}
|
||||
|
||||
func maskView() -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateValue(value: CGFloat?) {
|
||||
if value != self.value {
|
||||
let previousValue = self.value
|
||||
@ -501,6 +555,8 @@ private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateConte
|
||||
return true
|
||||
}
|
||||
|
||||
var requestUpdate: () -> Void = {}
|
||||
|
||||
init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
|
||||
self.value = value
|
||||
self.appearance = appearance
|
||||
@ -524,6 +580,10 @@ private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateConte
|
||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance)
|
||||
}
|
||||
|
||||
func maskView() -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func animate() {
|
||||
guard self.value < 1.0 else {
|
||||
return
|
||||
@ -553,8 +613,15 @@ private extension SemanticStatusNodeState {
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
if let current = current as? SemanticStatusNodeIconContext, current.icon == icon {
|
||||
return current
|
||||
if let current = current as? SemanticStatusNodeIconContext {
|
||||
if current.icon == icon {
|
||||
return current
|
||||
} else if (current.icon == .play && icon == .pause) || (current.icon == .pause && icon == .play) {
|
||||
current.icon = icon
|
||||
return current
|
||||
} else {
|
||||
return SemanticStatusNodeIconContext(icon: icon)
|
||||
}
|
||||
} else {
|
||||
return SemanticStatusNodeIconContext(icon: icon)
|
||||
}
|
||||
@ -874,6 +941,9 @@ public final class SemanticStatusNode: ASControlNode {
|
||||
self.state = state
|
||||
let previousStateContext = self.stateContext
|
||||
self.stateContext = self.state.context(current: self.stateContext)
|
||||
self.stateContext.requestUpdate = { [weak self] in
|
||||
self?.setNeedsDisplay()
|
||||
}
|
||||
|
||||
if animated && previousStateContext !== self.stateContext {
|
||||
self.transitionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, previousAppearanceContext: nil, completion: completion)
|
||||
@ -947,3 +1017,53 @@ public final class SemanticStatusNode: ASControlNode {
|
||||
parameters.appearanceState.drawForeground(context: context, size: bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
private enum PlayPauseIconNodeState: Equatable {
|
||||
case play
|
||||
case pause
|
||||
}
|
||||
|
||||
private final class PlayPauseIconNode: ManagedAnimationNode {
|
||||
private let duration: Double = 0.35
|
||||
private var iconState: PlayPauseIconNodeState = .play
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 36.0, height: 36.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
}
|
||||
|
||||
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
|
||||
guard self.iconState != state else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousState = self.iconState
|
||||
self.iconState = state
|
||||
|
||||
switch previousState {
|
||||
case .pause:
|
||||
switch state {
|
||||
case .play:
|
||||
if animated {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
}
|
||||
case .pause:
|
||||
break
|
||||
}
|
||||
case .play:
|
||||
switch state {
|
||||
case .pause:
|
||||
if animated {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
|
||||
}
|
||||
case .play:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
||||
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var items: [ChatListItem] = []
|
||||
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture in
|
||||
gesture?.cancel()
|
||||
|
@ -779,7 +779,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
||||
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var items: [ChatListItem] = []
|
||||
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture in
|
||||
gesture?.cancel()
|
||||
|
@ -356,7 +356,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var items: [ChatListItem] = []
|
||||
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, gesture in
|
||||
gesture?.cancel()
|
||||
|
@ -17,6 +17,7 @@ import AppBundle
|
||||
import ListMessageItem
|
||||
import AccountContext
|
||||
import ChatInterfaceState
|
||||
import ChatListUI
|
||||
|
||||
extension ChatReplyThreadMessage {
|
||||
var effectiveTopId: MessageId {
|
||||
@ -30,69 +31,6 @@ struct ChatTopVisibleMessageRange: Equatable {
|
||||
var isLast: Bool
|
||||
}
|
||||
|
||||
private class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
|
||||
private let selectionGestureActivationThreshold: CGFloat = 5.0
|
||||
|
||||
var recognized: Bool? = nil
|
||||
var initialLocation: CGPoint = CGPoint()
|
||||
|
||||
var shouldBegin: (() -> Bool)?
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.minimumNumberOfTouches = 2
|
||||
self.maximumNumberOfTouches = 2
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.recognized = nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let historyMessageCount: Int = 90
|
||||
|
||||
public enum ChatHistoryListDisplayHeaders {
|
||||
|
@ -710,20 +710,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
func animateFromMicInput(micInputNode: UIView, transition: CombinedTransition) -> ContextExtractedContentContainingNode? {
|
||||
for contentNode in self.contentNodes {
|
||||
if let contentNode = contentNode as? ChatMessageFileBubbleContentNode {
|
||||
if let statusContainerNode = contentNode.interactiveFileNode.statusContainerNode {
|
||||
let scale = statusContainerNode.contentRect.height / 100.0
|
||||
micInputNode.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
micInputNode.center = CGPoint(x: statusContainerNode.contentRect.midX, y: statusContainerNode.contentRect.midY)
|
||||
statusContainerNode.contentNode.view.addSubview(micInputNode)
|
||||
let statusContainerNode = contentNode.interactiveFileNode.statusContainerNode
|
||||
let scale = statusContainerNode.contentRect.height / 100.0
|
||||
micInputNode.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
micInputNode.center = CGPoint(x: statusContainerNode.contentRect.midX, y: statusContainerNode.contentRect.midY)
|
||||
statusContainerNode.contentNode.view.addSubview(micInputNode)
|
||||
|
||||
transition.horizontal.updateAlpha(layer: micInputNode.layer, alpha: 0.0, completion: { [weak micInputNode] _ in
|
||||
micInputNode?.removeFromSuperview()
|
||||
})
|
||||
transition.horizontal.updateAlpha(layer: micInputNode.layer, alpha: 0.0, completion: { [weak micInputNode] _ in
|
||||
micInputNode?.removeFromSuperview()
|
||||
})
|
||||
|
||||
transition.horizontal.animateTransformScale(node: statusContainerNode.contentNode, from: 1.0 / scale)
|
||||
transition.horizontal.animateTransformScale(node: statusContainerNode.contentNode, from: 1.0 / scale)
|
||||
|
||||
return statusContainerNode
|
||||
}
|
||||
return statusContainerNode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -39,9 +39,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private let consumableContentNode: ASImageNode
|
||||
|
||||
private var iconNode: TransformImageNode?
|
||||
private(set) var statusContainerNode: ContextExtractedContentContainingNode?
|
||||
let statusContainerNode: ContextExtractedContentContainingNode
|
||||
private var statusNode: SemanticStatusNode?
|
||||
private var playbackAudioLevelView: VoiceBlobView?
|
||||
private var playbackAudioLevelNode: VoiceBlobNode?
|
||||
private var streamingStatusNode: SemanticStatusNode?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
@ -73,7 +73,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
guard self.visibility != oldValue else { return }
|
||||
|
||||
if !self.visibility {
|
||||
self.playbackAudioLevelView?.stopAnimating()
|
||||
self.playbackAudioLevelNode?.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -140,9 +140,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.addSubnode(self.descriptionNode)
|
||||
self.addSubnode(self.fetchingTextNode)
|
||||
self.addSubnode(self.fetchingCompactTextNode)
|
||||
if let statusContainerNode = self.statusContainerNode {
|
||||
self.addSubnode(statusContainerNode)
|
||||
}
|
||||
self.addSubnode(self.statusContainerNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -673,7 +671,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
strongSelf.inputAudioLevel = CGFloat(value)
|
||||
strongSelf.playbackAudioLevelView?.updateLevel(CGFloat(value))
|
||||
strongSelf.playbackAudioLevelNode?.updateLevel(CGFloat(value))
|
||||
}))
|
||||
}
|
||||
|
||||
@ -692,19 +690,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview
|
||||
strongSelf.statusNode?.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
|
||||
strongSelf.statusContainerNode?.frame = progressFrame
|
||||
strongSelf.statusContainerNode?.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
strongSelf.statusContainerNode?.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
strongSelf.statusContainerNode.frame = progressFrame
|
||||
strongSelf.statusContainerNode.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
strongSelf.statusContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
|
||||
strongSelf.playbackAudioLevelView?.frame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||
strongSelf.playbackAudioLevelNode?.frame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||
strongSelf.progressFrame = progressFrame
|
||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||
strongSelf.fileIconImage = fileIconImage
|
||||
|
||||
strongSelf.statusContainerNode?.frame = progressFrame
|
||||
strongSelf.statusContainerNode?.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
strongSelf.statusContainerNode?.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
|
||||
if let updatedFetchControls = updatedFetchControls {
|
||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
if automaticDownload {
|
||||
@ -954,26 +948,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor, image: image, overlayForegroundNodeColor: presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor)
|
||||
self.statusNode = statusNode
|
||||
|
||||
self.statusContainerNode?.contentNode.insertSubnode(statusNode, at: 0)
|
||||
self.statusContainerNode?.frame = progressFrame
|
||||
self.statusContainerNode?.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
self.statusContainerNode?.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
self.statusContainerNode.contentNode.insertSubnode(statusNode, at: 0)
|
||||
self.statusContainerNode.frame = progressFrame
|
||||
self.statusContainerNode.contentRect = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
self.statusContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
statusNode.frame = CGRect(origin: CGPoint(), size: progressFrame.size)
|
||||
} else if let statusNode = self.statusNode {
|
||||
statusNode.backgroundNodeColor = backgroundNodeColor
|
||||
}
|
||||
|
||||
if state != .none && isVoice && self.playbackAudioLevelView == nil && false {
|
||||
if state != .none && isVoice && self.playbackAudioLevelNode == nil {
|
||||
let blobFrame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||
let playbackAudioLevelView = VoiceBlobView(
|
||||
frame: blobFrame,
|
||||
let playbackAudioLevelNode = VoiceBlobNode(
|
||||
maxLevel: 0.3,
|
||||
smallBlobRange: (0, 0),
|
||||
mediumBlobRange: (0.7, 0.8),
|
||||
bigBlobRange: (0.8, 0.9)
|
||||
)
|
||||
self.playbackAudioLevelView = playbackAudioLevelView
|
||||
self.view.addSubview(playbackAudioLevelView)
|
||||
playbackAudioLevelNode.isUserInteractionEnabled = false
|
||||
playbackAudioLevelNode.frame = blobFrame
|
||||
self.playbackAudioLevelNode = playbackAudioLevelNode
|
||||
self.insertSubnode(playbackAudioLevelNode, belowSubnode: self.statusContainerNode)
|
||||
|
||||
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
|
||||
let playbackMaskLayer = CAShapeLayer()
|
||||
@ -983,9 +978,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
|
||||
maskPath.append(UIBezierPath(rect: maskRect))
|
||||
playbackMaskLayer.path = maskPath.cgPath
|
||||
playbackAudioLevelView.layer.mask = playbackMaskLayer
|
||||
playbackAudioLevelNode.layer.mask = playbackMaskLayer
|
||||
}
|
||||
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
|
||||
self.playbackAudioLevelNode?.setColor(messageTheme.mediaActiveControlColor)
|
||||
|
||||
if streamingState != .none && self.streamingStatusNode == nil {
|
||||
let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor)
|
||||
@ -1012,9 +1007,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
switch state {
|
||||
case .pause:
|
||||
self.playbackAudioLevelView?.startAnimating()
|
||||
self.playbackAudioLevelNode?.startAnimating()
|
||||
default:
|
||||
self.playbackAudioLevelView?.stopAnimating()
|
||||
self.playbackAudioLevelNode?.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,6 +175,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, peerSelected: { _, _ in
|
||||
}, disabledPeerSelected: { _ in
|
||||
}, togglePeerSelected: { _ in
|
||||
}, togglePeersSelection: { _, _ in
|
||||
}, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { [weak self] peer, message, _ in
|
||||
if let strongSelf = self {
|
||||
|
Loading…
x
Reference in New Issue
Block a user