Various Improvements

This commit is contained in:
Ilya Laktyushin 2021-03-20 16:13:57 +05:00
parent cd6af7d537
commit 498dd0bbec
43 changed files with 792 additions and 231 deletions

View File

@ -52,8 +52,9 @@ public final class TelegramApplicationBindings {
public let getAvailableAlternateIcons: () -> [PresentationAppIcon]
public let getAlternateIconName: () -> String?
public let requestSetAlternateIconName: (String?, @escaping (Bool) -> Void) -> Void
public let forceOrientation: (UIInterfaceOrientation) -> Void
public init(isMainApp: Bool, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal<Bool, NoError>, applicationIsActive: Signal<Bool, NoError>, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void) {
public init(isMainApp: Bool, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal<Bool, NoError>, applicationIsActive: Signal<Bool, NoError>, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void, forceOrientation: @escaping (UIInterfaceOrientation) -> Void) {
self.isMainApp = isMainApp
self.containerPath = containerPath
self.appSpecificScheme = appSpecificScheme
@ -77,6 +78,7 @@ public final class TelegramApplicationBindings {
self.getAvailableAlternateIcons = getAvailableAlternateIcons
self.getAlternateIconName = getAlternateIconName
self.requestSetAlternateIconName = requestSetAlternateIconName
self.forceOrientation = forceOrientation
}
}
@ -471,15 +473,17 @@ public final class ContactSelectionControllerParams {
public let options: [ContactListAdditionalOption]
public let displayDeviceContacts: Bool
public let displayCallIcons: Bool
public let multipleSelection: Bool
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
public init(context: AccountContext, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }) {
public init(context: AccountContext, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }) {
self.context = context
self.autoDismiss = autoDismiss
self.title = title
self.options = options
self.displayDeviceContacts = displayDeviceContacts
self.displayCallIcons = displayCallIcons
self.multipleSelection = multipleSelection
self.confirmation = confirmation
}
}

View File

@ -3,7 +3,7 @@ import Display
import SwiftSignalKit
public protocol ContactSelectionController: ViewController {
var result: Signal<(ContactListPeer, ContactListAction)?, NoError> { get }
var result: Signal<([ContactListPeer], ContactListAction)?, NoError> { get }
var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { get set }

View File

@ -39,8 +39,9 @@ public final class PeerSelectionControllerParams {
public let attemptSelection: ((Peer) -> Void)?
public let createNewGroup: (() -> Void)?
public let pretendPresentedInModal: Bool
public let multipleSelection: Bool
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false) {
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false) {
self.context = context
self.filter = filter
self.hasChatListSelector = hasChatListSelector
@ -50,6 +51,7 @@ public final class PeerSelectionControllerParams {
self.attemptSelection = attemptSelection
self.createNewGroup = createNewGroup
self.pretendPresentedInModal = pretendPresentedInModal
self.multipleSelection = multipleSelection
}
}

View File

@ -365,9 +365,9 @@ public final class CallListController: TelegramBaseController {
controller.navigationPresentation = .modal
self.createActionDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller, weak self] peer in
|> deliverOnMainQueue).start(next: { [weak controller, weak self] result in
controller?.dismissSearch()
if let strongSelf = self, let (contactPeer, action) = peer, case let .peer(peer, _, _) = contactPeer {
if let strongSelf = self, let (contactPeers, action) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer {
strongSelf.call(peer.id, isVideo: action == .videoCall, began: {
if let strongSelf = self {
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()

View File

@ -240,7 +240,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
})]
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
interaction.openPeer(peer, .generic)
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction)
}
@ -594,10 +594,46 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
}
}
var index: Int = 0
var existingPeerIds = Set<ContactListPeerId>()
if let selectionState = selectionState {
for peer in selectionState.foundPeers {
if existingPeerIds.contains(peer.id) {
continue
}
existingPeerIds.insert(peer.id)
let selection: ContactsPeerItemSelection = .selectable(selected: selectionState.selectedPeerIndices[peer.id] != nil)
var presence: PeerPresence?
if case let .peer(peer, _, _) = peer {
presence = presences[peer.id]
}
let enabled: Bool
switch peer {
case let .peer(peer, _, _):
enabled = !disabledPeerIds.contains(peer.id)
default:
enabled = true
}
entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled))
index += 1
}
}
for i in 0 ..< orderedPeers.count {
let peer = orderedPeers[i]
if existingPeerIds.contains(peer.id) {
continue
}
existingPeerIds.insert(peer.id)
let selection: ContactsPeerItemSelection
if let selectionState = selectionState {
selection = .selectable(selected: selectionState.selectedPeerIndices[orderedPeers[i].id] != nil)
selection = .selectable(selected: selectionState.selectedPeerIndices[peer.id] != nil)
} else {
selection = .none
}
@ -606,20 +642,21 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
case .orderedByPresence:
header = commonHeader
default:
header = headers[orderedPeers[i].id]
header = headers[peer.id]
}
var presence: PeerPresence?
if case let .peer(peer, _, _) = orderedPeers[i] {
if case let .peer(peer, _, _) = peer {
presence = presences[peer.id]
}
let enabled: Bool
switch orderedPeers[i] {
switch peer {
case let .peer(peer, _, _):
enabled = !disabledPeerIds.contains(peer.id)
default:
enabled = true
}
entries.append(.peer(i, orderedPeers[i], presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled))
entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled))
index += 1
}
return entries
}
@ -706,15 +743,21 @@ public enum ContactListPresentation {
public struct ContactListNodeGroupSelectionState: Equatable {
public let selectedPeerIndices: [ContactListPeerId: Int]
public let foundPeers: [ContactListPeer]
public let selectedPeerMap: [ContactListPeerId: ContactListPeer]
public let nextSelectionIndex: Int
private init(selectedPeerIndices: [ContactListPeerId: Int], nextSelectionIndex: Int) {
private init(selectedPeerIndices: [ContactListPeerId: Int], foundPeers: [ContactListPeer], selectedPeerMap: [ContactListPeerId: ContactListPeer], nextSelectionIndex: Int) {
self.selectedPeerIndices = selectedPeerIndices
self.foundPeers = foundPeers
self.selectedPeerMap = selectedPeerMap
self.nextSelectionIndex = nextSelectionIndex
}
public init() {
self.selectedPeerIndices = [:]
self.foundPeers = []
self.selectedPeerMap = [:]
self.nextSelectionIndex = 0
}
@ -722,13 +765,21 @@ public struct ContactListNodeGroupSelectionState: Equatable {
var updatedIndices = self.selectedPeerIndices
if let _ = updatedIndices[peerId] {
updatedIndices.removeValue(forKey: peerId)
return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex)
return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, foundPeers: self.foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex)
} else {
updatedIndices[peerId] = self.nextSelectionIndex
return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, nextSelectionIndex: self.nextSelectionIndex + 1)
return ContactListNodeGroupSelectionState(selectedPeerIndices: updatedIndices, foundPeers: self.foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex + 1)
}
}
}
public func withFoundPeers(_ foundPeers: [ContactListPeer]) -> ContactListNodeGroupSelectionState {
return ContactListNodeGroupSelectionState(selectedPeerIndices: self.selectedPeerIndices, foundPeers: foundPeers, selectedPeerMap: self.selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex)
}
public func withSelectedPeerMap(_ selectedPeerMap: [ContactListPeerId: ContactListPeer]) -> ContactListNodeGroupSelectionState {
return ContactListNodeGroupSelectionState(selectedPeerIndices: self.selectedPeerIndices, foundPeers: self.foundPeers, selectedPeerMap: selectedPeerMap, nextSelectionIndex: self.nextSelectionIndex)
}
}
public final class ContactListNode: ASDisplayNode {
private let context: AccountContext
@ -754,11 +805,35 @@ public final class ContactListNode: ASDisplayNode {
private var selectionStateValue: ContactListNodeGroupSelectionState? {
didSet {
self.selectionStatePromise.set(.single(self.selectionStateValue))
self.selectionStateUpdated?(self.selectionStateValue)
}
}
public var selectionState: ContactListNodeGroupSelectionState? {
return self.selectionStateValue
}
public var selectionStateUpdated: ((ContactListNodeGroupSelectionState?) -> Void)?
public var selectedPeers: [ContactListPeer] {
if let selectionState = self.selectionState {
var selectedPeers: [ContactListPeer] = []
var selectedIndices: [(Int, ContactListPeerId)] = []
for (id, index) in selectionState.selectedPeerIndices {
selectedIndices.append((index, id))
}
selectedIndices.sort(by: { lhs, rhs in
return lhs.0 < rhs.0
})
for (_, id) in selectedIndices {
if let peer = selectionState.selectedPeerMap[id] {
selectedPeers.append(peer)
}
}
return selectedPeers
} else {
return []
}
}
private var interaction: ContactListNodeInteraction?
private var enableUpdatesValue = false
@ -864,6 +939,7 @@ public final class ContactListNode: ASDisplayNode {
let processingQueue = Queue()
let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil)
let previousSelectionState = Atomic<ContactListNodeGroupSelectionState?>(value: nil)
let interaction = ContactListNodeInteraction(activateSearch: { [weak self] in
self?.activateSearch?()
@ -874,7 +950,21 @@ public final class ContactListNode: ASDisplayNode {
}, suppressWarning: { [weak self] in
self?.suppressPermissionWarning?()
}, openPeer: { [weak self] peer, action in
self?.openPeer?(peer, action)
if let strongSelf = self {
if let _ = strongSelf.selectionStateValue {
strongSelf.updateSelectionState({ state in
if let state = state {
var selectedPeerMap = state.selectedPeerMap
selectedPeerMap[peer.id] = peer
return state.withToggledPeerId(peer.id).withSelectedPeerMap(selectedPeerMap)
} else {
return nil
}
})
} else {
strongSelf.openPeer?(peer, action)
}
}
}, contextAction: contextAction)
self.indexNode.indexSelected = { [weak self] section in
@ -1037,6 +1127,15 @@ public final class ContactListNode: ASDisplayNode {
var peers: [ContactListPeer] = []
if let selectionState = selectionState {
for peer in selectionState.foundPeers {
if case let .peer(peer, _, _) = peer {
existingPeerIds.insert(peer.id)
}
peers.append(peer)
}
}
if !excludeSelf && !existingPeerIds.contains(accountPeer.id) {
let lowercasedQuery = query.lowercased()
if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) {
@ -1210,6 +1309,7 @@ public final class ContactListNode: ASDisplayNode {
}
let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons)
let previous = previousEntries.swap(entries)
let previousSelection = previousSelectionState.swap(selectionState)
var hadPermissionInfo = false
if let previous = previous {
@ -1229,10 +1329,11 @@ public final class ContactListNode: ASDisplayNode {
}
let animation: ContactListAnimation
if hadPermissionInfo != hasPermissionInfo {
if (previousSelection == nil) != (selectionState == nil) {
animation = .insertion
}
else if let previous = previous, !presentationData.disableAnimations, (entries.count - previous.count) < 20 {
} else if hadPermissionInfo != hasPermissionInfo {
animation = .insertion
} else if let previous = previous, !presentationData.disableAnimations, (entries.count - previous.count) < 20 {
animation = .default
} else {
animation = .none
@ -1337,7 +1438,6 @@ public final class ContactListNode: ASDisplayNode {
}
public func updateSelectedChatLocation(_ chatLocation: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
self.interaction?.itemHighlighting.chatLocation = chatLocation
self.interaction?.itemHighlighting.progress = progress
@ -1352,7 +1452,7 @@ public final class ContactListNode: ASDisplayNode {
self.disposable.dispose()
self.presentationDataDisposable?.dispose()
}
public func updateSelectionState(_ f: (ContactListNodeGroupSelectionState?) -> ContactListNodeGroupSelectionState?) {
let updatedSelectionState = f(self.selectionStateValue)
if updatedSelectionState != self.selectionStateValue {

View File

@ -40,6 +40,11 @@ public enum ContactsPeerItemSelection: Equatable {
case selectable(selected: Bool)
}
public enum ContactsPeerItemSelectionPosition: Equatable {
case left
case right
}
public struct ContactsPeerItemEditing: Equatable {
public var editable: Bool
public var editing: Bool
@ -130,6 +135,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let badge: ContactsPeerItemBadge?
let enabled: Bool
let selection: ContactsPeerItemSelection
let selectionPosition: ContactsPeerItemSelectionPosition
let editing: ContactsPeerItemEditing
let options: [ItemListPeerItemRevealOption]
let additionalActions: [ContactsPeerItemAction]
@ -148,7 +154,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
public let header: ListViewItemHeader?
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, arrowAction: (() -> Void)? = nil) {
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, selectionPosition: ContactsPeerItemSelectionPosition = .right, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, arrowAction: (() -> Void)? = nil) {
self.presentationData = presentationData
self.style = style
self.sectionId = sectionId
@ -161,6 +167,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.badge = badge
self.enabled = enabled
self.selection = selection
self.selectionPosition = selectionPosition
self.editing = editing
self.options = options
self.additionalActions = additionalActions
@ -518,7 +525,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let leftInset: CGFloat = 65.0 + params.leftInset
var leftInset: CGFloat = 65.0 + params.leftInset
var rightInset: CGFloat = 10.0 + params.rightInset
let updatedSelectionNode: CheckNode?
@ -527,7 +534,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
case .none:
updatedSelectionNode = nil
case let .selectable(selected):
rightInset += 38.0
switch item.selectionPosition {
case .left:
leftInset += 38.0
case .right:
rightInset += 38.0
}
isSelected = selected
let selectionNode: CheckNode
@ -999,14 +1011,29 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
if let updatedSelectionNode = updatedSelectionNode {
let hadSelectionNode = strongSelf.selectionNode != nil
if strongSelf.selectionNode !== updatedSelectionNode {
strongSelf.selectionNode?.removeFromSupernode()
strongSelf.selectionNode = updatedSelectionNode
strongSelf.addSubnode(updatedSelectionNode)
}
updatedSelectionNode.setSelected(isSelected, animated: animated)
updatedSelectionNode.setSelected(isSelected, animated: true)
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 22.0 - 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0))
switch item.selectionPosition {
case .left:
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0))
case .right:
updatedSelectionNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 22.0 - 17.0, y: floor((nodeLayout.contentSize.height - 22.0) / 2.0)), size: CGSize(width: 22.0, height: 22.0))
}
if !hadSelectionNode {
switch item.selectionPosition {
case .left:
transition.animateFrame(node: updatedSelectionNode, from: updatedSelectionNode.frame.offsetBy(dx: -38.0, dy: 0.0))
case .right:
transition.animateFrame(node: updatedSelectionNode, from: updatedSelectionNode.frame.offsetBy(dx: 38.0, dy: 0.0))
}
}
} else if let selectionNode = strongSelf.selectionNode {
selectionNode.removeFromSupernode()
strongSelf.selectionNode = nil

View File

@ -225,7 +225,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
return size
} else if let imageNode = self.imageNode {
let nodeSize = imageNode.image?.size ?? CGSize()
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height))
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(44.0, max(nodeSize.height, superSize.height)))
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize)
imageNode.frame = imageFrame
self.imageRippleNode.frame = imageFrame

View File

@ -107,6 +107,8 @@ class CaptionScrollWrapperNode: ASDisplayNode {
}
}
final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
@ -126,8 +128,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private let textNode: ImmediateTextNode
private let authorNameNode: ASTextNode
private let dateNode: ASTextNode
private let backwardButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode
private let backwardButton: PlaybackButtonNode
private let forwardButton: PlaybackButtonNode
private let playbackControlButton: HighlightableButtonNode
private let playPauseIconNode: PlayPauseIconNode
@ -188,7 +190,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var statusState: RadialStatusNodeState = .none
switch status {
case let .Fetching(isActive, progress):
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
statusState = .cloudProgress(color: UIColor.white, strokeBackgroundColor: UIColor.white.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress))
case .Local:
@ -207,7 +209,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.backwardButton.isHidden = !seekable
self.forwardButton.isHidden = !seekable
self.playbackControlButton.isHidden = false
self.playPauseIconNode.enqueueState(paused && !self.wasPlaying ? .play : .pause, animated: true)
let icon: PlayPauseIconNodeState
if let wasPlaying = self.wasPlaying {
icon = wasPlaying ? .pause : .play
} else {
icon = paused ? .play : .pause
}
self.playPauseIconNode.enqueueState(icon, animated: true)
self.statusButtonNode.isHidden = true
self.statusNode.isHidden = true
}
@ -303,13 +312,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = false
self.backwardButton = HighlightableButtonNode()
self.backwardButton = PlaybackButtonNode()
self.backwardButton.isHidden = true
self.backwardButton.setImage(backwardImage, for: [])
self.backwardButton.backgroundIconNode.image = backwardImage
self.forwardButton = HighlightableButtonNode()
self.forwardButton = PlaybackButtonNode()
self.forwardButton.isHidden = true
self.forwardButton.setImage(forwardImage, for: [])
self.forwardButton.forward = true
self.forwardButton.backgroundIconNode.image = forwardImage
self.playbackControlButton = HighlightableButtonNode()
self.playbackControlButton.isHidden = true
@ -408,12 +418,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.forwardButton.view.addGestureRecognizer(forwardLongPressGestureRecognizer)
}
private var wasPlaying = false
private var wasPlaying: Bool?
@objc private func seekBackwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
self.backwardButton.isPressing = true
self.wasPlaying = !self.currentIsPaused
if self.wasPlaying {
if self.wasPlaying == true {
self.playbackControl?()
}
@ -434,12 +445,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.seekTimer = seekTimer
seekTimer.start()
case .ended, .cancelled:
self.backwardButton.isPressing = false
self.seekTimer?.invalidate()
self.seekTimer = nil
if self.wasPlaying {
if self.wasPlaying == true {
self.playbackControl?()
self.wasPlaying = false
}
self.wasPlaying = nil
default:
break
}
@ -448,8 +460,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
@objc private func seekForwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
self.forwardButton.isPressing = true
self.wasPlaying = !self.currentIsPaused
if !self.wasPlaying {
if self.wasPlaying == false {
self.playbackControl?()
}
@ -470,13 +483,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.seekTimer = seekTimer
seekTimer.start()
case .ended, .cancelled:
self.forwardButton.isPressing = false
self.setPlayRate?(1.0)
self.seekTimer?.invalidate()
self.seekTimer = nil
if !self.wasPlaying {
if self.wasPlaying == false {
self.playbackControl?()
}
self.wasPlaying = nil
default:
break
}
@ -762,10 +777,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.deleteButton.frame = deleteFrame
self.editButton.frame = editFrame
if let image = self.backwardButton.image(for: .normal) {
if let image = self.backwardButton.backgroundIconNode.image {
self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
}
if let image = self.forwardButton.image(for: .normal) {
if let image = self.forwardButton.backgroundIconNode.image {
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
}
@ -1392,7 +1407,7 @@ private enum PlayPauseIconNodeState: Equatable {
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .pause
init() {
@ -1435,3 +1450,68 @@ private final class PlayPauseIconNode: ManagedAnimationNode {
}
}
}
private let circleDiameter: CGFloat = 80.0
private final class PlaybackButtonNode: HighlightTrackingButtonNode {
let backgroundIconNode: ASImageNode
let textNode: ImmediateTextNode
var forward: Bool = false
var isPressing = false {
didSet {
if self.isPressing != oldValue && !self.isPressing {
self.highligthedChanged(false)
}
}
}
init() {
self.backgroundIconNode = ASImageNode()
self.backgroundIconNode.isLayerBacked = true
self.backgroundIconNode.displaysAsynchronously = false
self.backgroundIconNode.displayWithoutProcessing = true
self.textNode = ImmediateTextNode()
self.textNode.attributedText = NSAttributedString(string: "15", font: Font.with(size: 11.0, design: .round, weight: .semibold, traits: []), textColor: .white)
super.init(pointerStyle: .circle)
self.addSubnode(self.backgroundIconNode)
self.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundIconNode.alpha = 0.4
strongSelf.textNode.layer.removeAnimation(forKey: "opacity")
strongSelf.textNode.alpha = 0.4
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear)
transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: strongSelf.forward ? CGFloat.pi / 4.0 : -CGFloat.pi / 4.0)
} else if !strongSelf.isPressing {
strongSelf.backgroundIconNode.alpha = 1.0
strongSelf.backgroundIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.textNode.alpha = 1.0
strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .linear)
transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: 0.0)
}
}
}
}
override func layout() {
super.layout()
self.backgroundIconNode.frame = self.bounds
let size = self.bounds.size
let textSize = self.textNode.updateLayout(size)
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0) + UIScreenPixel), size: textSize)
}
}

View File

@ -54,12 +54,14 @@ public final class GalleryFooterNode: ASDisplayNode {
}
}
var animateOverlayIn = false
var dismissedCurrentOverlayContentNode: GalleryOverlayContentNode?
if self.currentOverlayContentNode !== overlayContentNode {
if let currentOverlayContentNode = self.currentOverlayContentNode {
dismissedCurrentOverlayContentNode = currentOverlayContentNode
}
self.currentOverlayContentNode = overlayContentNode
animateOverlayIn = true
if let overlayContentNode = overlayContentNode {
overlayContentNode.setVisibilityAlpha(self.visibilityAlpha)
self.addSubnode(overlayContentNode)
@ -96,7 +98,9 @@ public final class GalleryFooterNode: ASDisplayNode {
overlayContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: backgroundHeight, transition: transition)
transition.updateFrame(node: overlayContentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
overlayContentNode.animateIn(previousContentNode: dismissedCurrentOverlayContentNode, transition: contentTransition)
if animateOverlayIn {
overlayContentNode.animateIn(previousContentNode: dismissedCurrentOverlayContentNode, transition: contentTransition)
}
if let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode {
dismissedCurrentOverlayContentNode.animateOut(nextContentNode: overlayContentNode, transition: contentTransition, completion: { [weak self, weak dismissedCurrentOverlayContentNode] in
if let strongSelf = self, let dismissedCurrentOverlayContentNode = dismissedCurrentOverlayContentNode, dismissedCurrentOverlayContentNode !== strongSelf.currentOverlayContentNode {

View File

@ -164,68 +164,61 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode
}
}
private let soundOnImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOn"), color: .white)
private let soundOffImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOff"), color: .white)
private var roundButtonBackgroundImage = {
return generateImage(CGSize(width: 42.0, height: 42), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
context.fillEllipse(in: bounds)
})
}()
private let fullscreenImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Fullscreen"), color: .white)
private let minimizeImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Minimize"), color: .white)
private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode {
private let soundButtonNode: HighlightableButtonNode
private let wrapperNode: ASDisplayNode
private let fullscreenNode: HighlightableButtonNode
private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat)?
var action: ((Bool) -> Void)?
override init() {
self.soundButtonNode = HighlightableButtonNode()
self.soundButtonNode.alpha = 0.0
self.soundButtonNode.setBackgroundImage(roundButtonBackgroundImage, for: .normal)
self.soundButtonNode.setImage(soundOffImage, for: .normal)
self.soundButtonNode.setImage(soundOnImage, for: .selected)
self.soundButtonNode.setImage(soundOnImage, for: [.selected, .highlighted])
self.wrapperNode = ASDisplayNode()
self.wrapperNode.alpha = 0.0
self.fullscreenNode = HighlightableButtonNode()
self.fullscreenNode.setImage(fullscreenImage, for: .normal)
self.fullscreenNode.setImage(minimizeImage, for: .selected)
self.fullscreenNode.setImage(minimizeImage, for: [.selected, .highlighted])
super.init()
self.soundButtonNode.addTarget(self, action: #selector(self.soundButtonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.soundButtonNode)
}
func hide() {
self.soundButtonNode.isHidden = true
self.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.fullscreenNode)
self.fullscreenNode.addTarget(self, action: #selector(self.soundButtonPressed), forControlEvents: .touchUpInside)
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, metrics, leftInset, rightInset, bottomInset)
let soundButtonDiameter: CGFloat = 42.0
let inset: CGFloat = 12.0
let effectiveBottomInset = self.visibilityAlpha < 1.0 ? 0.0 : bottomInset
let soundButtonFrame = CGRect(origin: CGPoint(x: size.width - soundButtonDiameter - inset - rightInset, y: size.height - soundButtonDiameter - inset - effectiveBottomInset), size: CGSize(width: soundButtonDiameter, height: soundButtonDiameter))
transition.updateFrame(node: self.soundButtonNode, frame: soundButtonFrame)
let isLandscape = size.width > size.height
self.fullscreenNode.isSelected = isLandscape
let iconSize: CGFloat = 42.0
let inset: CGFloat = 4.0
let buttonFrame = CGRect(origin: CGPoint(x: size.width - iconSize - inset - rightInset, y: size.height - iconSize - inset - bottomInset), size: CGSize(width: iconSize, height: iconSize))
transition.updateFrame(node: self.wrapperNode, frame: buttonFrame)
transition.updateFrame(node: self.fullscreenNode, frame: CGRect(origin: CGPoint(), size: buttonFrame.size))
}
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.soundButtonNode, alpha: 1.0)
transition.updateAlpha(node: self.wrapperNode, alpha: 1.0)
}
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
transition.updateAlpha(node: self.soundButtonNode, alpha: 0.0)
transition.updateAlpha(node: self.wrapperNode, alpha: 0.0)
}
override func setVisibilityAlpha(_ alpha: CGFloat) {
super.setVisibilityAlpha(alpha)
self.updateSoundButtonVisibility()
self.updateFullscreenButtonVisibility()
}
func updateSoundButtonVisibility() {
if self.soundButtonNode.isSelected {
self.soundButtonNode.alpha = self.visibilityAlpha
} else {
self.soundButtonNode.alpha = 1.0
}
func updateFullscreenButtonVisibility() {
self.wrapperNode.alpha = self.visibilityAlpha
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, transition: .animated(duration: 0.3, curve: .easeInOut))
@ -233,12 +226,18 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
}
@objc func soundButtonPressed() {
self.soundButtonNode.isSelected = !self.soundButtonNode.isSelected
self.updateSoundButtonVisibility()
var toLandscape = false
if let (size, _, _, _ ,_) = self.validLayout, size.width < size.height {
toLandscape = true
}
if toLandscape {
self.wrapperNode.alpha = 0.0
}
self.action?(toLandscape)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.soundButtonNode.frame.contains(point) {
if !self.wrapperNode.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
@ -324,6 +323,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
super.init()
self.overlayContentNode.action = { [weak self] toLandscape in
self?.updateControlsVisibility(!toLandscape)
context.sharedContext.applicationBindings.forceOrientation(toLandscape ? .landscapeRight : .portrait)
}
self.scrubberView.seek = { [weak self] timecode in
self?.videoNode?.seek(timecode)
}
@ -484,6 +488,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var previousPlaying: Bool?
private func setupControlsTimer() {
return
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
self?.updateControlsVisibility(false)
self?.controlsTimer = nil
@ -500,6 +505,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.statusButtonNode.isHidden = true
}
let dimensions = item.content.dimensions
if dimensions.height > 0.0 {
if dimensions.width / dimensions.height >= 1.33 {
self.overlayContentNode.isHidden = true
}
}
self.dismissOnOrientationChange = item.landscape
var hasLinkedStickers = false
@ -576,7 +588,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
if item.fromPlayingVideo {
videoNode.canAttachContent = false
self.overlayContentNode.hide()
} else {
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
}
@ -1630,6 +1641,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
return .single((self.footerContentNode, self.overlayContentNode))
}
}

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/ContextUI:ContextUI",
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
],
visibility = [
"//visibility:public",

View File

@ -19,6 +19,7 @@ import MusicAlbumArtResources
import UniversalMediaPlayer
import ContextUI
import FileMediaResourceStatus
import ManagedAnimationNode
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
@ -184,7 +185,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
private let playbackStatusDisposable = MetaDisposable()
private let playbackStatus = Promise<MediaPlayerStatus>()
private var downloadStatusIconNode: ASImageNode
private var downloadStatusIconNode: DownloadIconNode
private var linearProgressNode: LinearProgressNode?
private var context: AccountContext?
@ -246,10 +247,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
self.iconStatusNode.isUserInteractionEnabled = false
self.downloadStatusIconNode = ASImageNode()
self.downloadStatusIconNode.isLayerBacked = true
self.downloadStatusIconNode.displaysAsynchronously = false
self.downloadStatusIconNode.displayWithoutProcessing = true
self.downloadStatusIconNode = DownloadIconNode()
self.restrictionNode = ASDisplayNode()
self.restrictionNode.isHidden = true
@ -738,6 +736,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
strongSelf.linearProgressNode?.updateTheme(theme: item.presentationData.theme.theme)
strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6)
strongSelf.downloadStatusIconNode.customColor = item.presentationData.theme.theme.list.itemAccentColor
}
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
@ -849,7 +849,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
}))
}
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 12.0) / 2.0)), size: CGSize(width: 12.0, height: 12.0)))
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 18.0) / 2.0)), size: CGSize(width: 18.0, height: 18.0)))
if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
@ -1015,10 +1015,12 @@ public final class ListMessageFileItemNode: ListMessageNode {
transition.updateFrame(node: linearProgressNode, frame: progressFrame)
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
var animated = true
if self.downloadStatusIconNode.supernode == nil {
animated = false
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
}
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.presentationData.theme.theme)
self.downloadStatusIconNode.enqueueState(.pause, animated: animated)
case .Local:
if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode = nil
@ -1031,7 +1033,6 @@ public final class ListMessageFileItemNode: ListMessageNode {
if self.downloadStatusIconNode.supernode != nil {
self.downloadStatusIconNode.removeFromSupernode()
}
self.downloadStatusIconNode.image = nil
case .Remote:
if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode = nil
@ -1039,10 +1040,12 @@ public final class ListMessageFileItemNode: ListMessageNode {
linearProgressNode?.removeFromSupernode()
})
}
var animated = true
if self.downloadStatusIconNode.supernode == nil {
animated = false
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
}
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.presentationData.theme.theme)
self.downloadStatusIconNode.enqueueState(.download, animated: animated)
}
} else {
if let linearProgressNode = self.linearProgressNode {
@ -1063,18 +1066,18 @@ public final class ListMessageFileItemNode: ListMessageNode {
transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame)
}
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
if downloadingString != nil {
self.descriptionProgressNode.isHidden = false
self.descriptionNode.isHidden = true
alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 0.0)
} else {
self.descriptionProgressNode.isHidden = true
self.descriptionNode.isHidden = false
alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 1.0)
}
let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
self.descriptionProgressNode.attributedText = NSAttributedString(string: downloadingString ?? "", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
let descriptionSize = self.descriptionProgressNode.updateLayout(CGSize(width: size.width - 14.0, height: size.height))
transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize))
}
func activateMedia() {
@ -1269,3 +1272,53 @@ private final class LinearProgressNode: ASDisplayNode {
self.shimmerNode.frame = CGRect(origin: CGPoint(x: shimmerOffset - shimmerWidth / 2.0, y: 0.0), size: CGSize(width: shimmerWidth, height: 3.0))
}
}
private enum DownloadIconNodeState: Equatable {
case download
case pause
}
private final class DownloadIconNode: ManagedAnimationNode {
private let duration: Double = 0.3
private var iconState: DownloadIconNodeState = .download
init() {
super.init(size: CGSize(width: 18.0, height: 18.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
func enqueueState(_ state: DownloadIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .download:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .download:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01))
}
case .download:
break
}
}
}
}

View File

@ -1322,8 +1322,8 @@ private func addContactToExisting(context: AccountContext, parentController: Vie
contactsController.navigationPresentation = .modal
(parentController.navigationController as? NavigationController)?.pushViewController(contactsController)
let _ = (contactsController.result
|> deliverOnMainQueue).start(next: { peer in
if let (peer, _) = peer {
|> deliverOnMainQueue).start(next: { result in
if let (peers, _) = result, let peer = peers.first {
let dataSignal: Signal<(Peer?, DeviceContactStableId?), NoError>
switch peer {
case let .peer(contact, _, _):

View File

@ -138,6 +138,7 @@ public final class SelectablePeerNode: ASDisplayNode {
}
public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: RenderedPeer, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
let isFirstTime = self.peer == nil
self.peer = peer
guard let mainPeer = peer.chatMainPeer else {
return
@ -165,7 +166,7 @@ public final class SelectablePeerNode: ASDisplayNode {
let onlineLayout = self.onlineNode.asyncLayout()
let (onlineSize, onlineApply) = onlineLayout(online, false)
let _ = onlineApply(false)
let _ = onlineApply(!isFirstTime)
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel), color: nil, transition: .immediate)
self.onlineNode.frame = CGRect(origin: CGPoint(), size: onlineSize)

View File

@ -10,6 +10,9 @@ swift_library(
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/GZip:GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/AppBundle:AppBundle",
],
visibility = [
"//visibility:public",

View File

@ -3,6 +3,9 @@ import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import RLottieBinding
import GZip
import AppBundle
public enum SemanticStatusNodeState: Equatable {
public struct ProgressAppearance: Equatable {
@ -88,10 +91,22 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
let transitionFraction: CGFloat
let icon: SemanticStatusNodeIcon
private let instance: LottieInstance?
private let renderContext: DrawingContext?
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon) {
self.transitionFraction = transitionFraction
self.icon = icon
let displaySize = CGSize(width: 44.0, height: 44.0)
if let path = getAppBundle().path(forResource: "anim_playpause", ofType: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024), let instance = LottieInstance(data: unpackedData, cacheKey: "anim_playpause") {
self.instance = instance
self.renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
} else {
self.instance = nil
self.renderContext = nil
}
super.init()
}

View File

@ -496,7 +496,7 @@ private enum PlayPauseIconNodeState: Equatable {
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .pause
init() {

View File

@ -1282,7 +1282,7 @@ public final class VoiceChatController: ViewController {
}
})
f(.default)
f(.dismissWithoutContent)
})))
if let callState = strongSelf.callState, (callState.canManageCall && !callState.adminIds.contains(peer.id) && peer.id.namespace != Namespaces.Peer.CloudChannel) {
@ -2117,6 +2117,11 @@ public final class VoiceChatController: ViewController {
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: self.presentationData.strings.VoiceChat_LeaveConfirmation))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
action()
}))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.VoiceChat_LeaveVoiceChat, color: .accent, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
@ -2130,12 +2135,6 @@ public final class VoiceChatController: ViewController {
}))
}))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.VoiceChat_LeaveAndEndVoiceChat, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
action()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [

View File

@ -233,7 +233,7 @@ public final class PermissionContentNode: ASDisplayNode {
}
if let _ = self.animationNode, size.width < size.height {
imageSpacing = floor(availableHeight * 0.12)
imageSize = CGSize(width: 200.0, height: 200.0)
imageSize = CGSize(width: 240.0, height: 240.0)
contentHeight += imageSize.height + imageSpacing
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -662,6 +662,10 @@ final class SharedApplicationContext {
} else {
completion(false)
}
}, forceOrientation: { orientation in
let value = orientation.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
})
let accountManagerSignal = Signal<AccountManager, NoError> { subscriber in

View File

@ -1436,6 +1436,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self?.commitPurposefulAction()
}
}
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .linkCopied(text: strongSelf.presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
}
shareController.completed = { [weak self] peerIds in
if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
@ -8854,58 +8859,37 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
private func presentContactPicker() {
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, title: { $0.Contacts_Title }, displayDeviceContacts: true))
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true))
contactsController.navigationPresentation = .modal
self.chatDisplayNode.dismissInput()
self.effectiveNavigationController?.pushViewController(contactsController)
self.controllerNavigationDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self, let (peer, _) = peer {
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
return
}
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let context = strongSelf.context
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
|> take(1)
|> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in
var stableId: String?
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
outer: for (id, data) in basicData {
for phoneNumber in data.phoneNumbers {
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
stableId = id
break outer
}
|> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self, let (peers, _) = peers {
if peers.count > 1 {
var enqueueMessages: [EnqueueMessage] = []
for peer in peers {
var media: TelegramMediaContact?
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
continue
}
}
if let stableId = stableId {
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|> take(1)
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
return (contact, extendedData)
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let phone = contactData.basicData.phoneNumbers[0].value
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil)
case let .deviceContact(_, basicData):
guard !basicData.phoneNumbers.isEmpty else {
continue
}
} else {
return .single((contact, contactData))
}
let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let phone = contactData.basicData.phoneNumbers[0].value
media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil)
}
case let .deviceContact(id, _):
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|> take(1)
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
return (nil, extendedData)
}
}
strongSelf.controllerNavigationDisposable.set((dataSignal
|> deliverOnMainQueue).start(next: { peerAndContactData in
if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 {
if contactData.isPrimitive {
let phone = contactData.basicData.phoneNumbers[0].value
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil)
if let media = media {
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
@ -8915,31 +8899,91 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil)
strongSelf.sendMessages([message])
} else {
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
return
}
let phone = contactData.basicData.phoneNumbers[0].value
if let vCardData = contactData.serializedVCard() {
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData)
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
})
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil)
strongSelf.sendMessages([message])
}
}), completed: nil, cancelled: nil)
strongSelf.effectiveNavigationController?.pushViewController(contactController)
enqueueMessages.append(message)
}
}
}))
strongSelf.sendMessages(enqueueMessages)
} else if let peer = peers.first {
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
return
}
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let context = strongSelf.context
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
|> take(1)
|> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in
var stableId: String?
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
outer: for (id, data) in basicData {
for phoneNumber in data.phoneNumbers {
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
stableId = id
break outer
}
}
}
if let stableId = stableId {
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|> take(1)
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
return (contact, extendedData)
}
} else {
return .single((contact, contactData))
}
}
case let .deviceContact(id, _):
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|> take(1)
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
return (nil, extendedData)
}
}
strongSelf.controllerNavigationDisposable.set((dataSignal
|> deliverOnMainQueue).start(next: { peerAndContactData in
if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 {
if contactData.isPrimitive {
let phone = contactData.basicData.phoneNumbers[0].value
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil)
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
})
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil)
strongSelf.sendMessages([message])
} else {
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
return
}
let phone = contactData.basicData.phoneNumbers[0].value
if let vCardData = contactData.serializedVCard() {
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData)
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
})
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil)
strongSelf.sendMessages([message])
}
}), completed: nil, cancelled: nil)
strongSelf.effectiveNavigationController?.pushViewController(contactController)
}
}
}))
}
}
}))
}
@ -10481,7 +10525,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var attemptSelectionImpl: ((Peer) -> Void)?
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: filter, attemptSelection: { peer in
attemptSelectionImpl?(peer)
}))
}, multipleSelection: true))
let context = self.context
attemptSelectionImpl = { [weak controller] peer in
guard let controller = controller else {

View File

@ -308,7 +308,7 @@ private enum PlayPauseIconNodeState: Equatable {
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .pause
init() {

View File

@ -157,8 +157,8 @@ public class ComposeControllerImpl: ViewController, ComposeController {
let controller = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, autoDismiss: false, title: { $0.Compose_NewEncryptedChatTitle }))
strongSelf.createActionDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] peer in
if let strongSelf = self, let (contactPeer, _) = peer, case let .peer(peer, _, _) = contactPeer {
|> deliverOnMainQueue).start(next: { [weak controller] result in
if let strongSelf = self, let (contactPeers, _) = result, case let .peer(peer, _, _) = contactPeers.first {
controller?.dismissSearch()
controller?.displayNavigationActivity = true
strongSelf.createActionDisposable.set((createSecretChat(account: strongSelf.context.account, peerId: peer.id) |> deliverOnMainQueue).start(next: { peerId in

View File

@ -41,8 +41,8 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
return self._ready
}
private let _result = Promise<(ContactListPeer, ContactListAction)?>()
var result: Signal<(ContactListPeer, ContactListAction)?, NoError> {
private let _result = Promise<([ContactListPeer], ContactListAction)?>()
var result: Signal<([ContactListPeer], ContactListAction)?, NoError> {
return self._result.get()
}
@ -118,6 +118,10 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
if params.multipleSelection {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.beginSelection))
}
}
required init(coder aDecoder: NSCoder) {
@ -129,6 +133,11 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.presentationDataDisposable?.dispose()
}
@objc private func beginSelection() {
self.navigationItem.rightBarButtonItem = nil
self.contactsNode.beginSelection()
}
private func updateThemeAndStrings() {
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
@ -165,7 +174,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.contactsNode.contactListNode.openPeer = { [weak self] peer, action in
self?.openPeer(peer: peer, action: action)
}
self.contactsNode.contactListNode.suppressPermissionWarning = { [weak self] in
if let strongSelf = self {
strongSelf.context.sharedContext.presentContactsWarningSuppression(context: strongSelf.context, present: { c, a in
@ -191,6 +200,16 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
return false
}
}
self.contactsNode.requestMultipleAction = { [weak self] in
if let strongSelf = self {
let selectedPeers = strongSelf.contactsNode.contactListNode.selectedPeers
strongSelf._result.set(.single((selectedPeers, .generic)))
if strongSelf.autoDismiss {
strongSelf.dismiss()
}
}
}
self.displayNodeDidLoad()
}
@ -263,7 +282,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf._result.set(.single((peer, action)))
strongSelf._result.set(.single(([peer], action)))
if strongSelf.autoDismiss {
strongSelf.dismiss()
}

View File

@ -10,6 +10,7 @@ import AccountContext
import SearchBarNode
import ContactListUI
import SearchUI
import SolidRoundedButtonNode
final class ContactSelectionControllerNode: ASDisplayNode {
var displayProgress: Bool = false {
@ -30,17 +31,22 @@ final class ContactSelectionControllerNode: ASDisplayNode {
private let context: AccountContext
private var searchDisplayController: SearchDisplayController?
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)?
var navigationBar: NavigationBar?
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)?
var requestMultipleAction: (() -> Void)?
var dismiss: (() -> Void)?
var presentationData: PresentationData
var presentationDataDisposable: Disposable?
private let countPanelNode: ContactSelectionCountPanelNode
private var selectionState: ContactListNodeGroupSelectionState?
init(context: AccountContext, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -51,6 +57,11 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.dimNode = ASDisplayNode()
var shareImpl: (() -> Void)?
self.countPanelNode = ContactSelectionCountPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: {
shareImpl?()
})
super.init()
self.setViewBlock({
@ -76,12 +87,37 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.dimNode.alpha = 0.0
self.dimNode.isUserInteractionEnabled = false
self.addSubnode(self.dimNode)
self.addSubnode(self.countPanelNode)
self.contactListNode.selectionStateUpdated = { [weak self] selectionState in
if let strongSelf = self {
strongSelf.countPanelNode.count = selectionState?.selectedPeerIndices.count ?? 0
let previousState = strongSelf.selectionState
strongSelf.selectionState = selectionState
if previousState?.selectedPeerIndices.isEmpty != strongSelf.selectionState?.selectedPeerIndices.isEmpty {
if let (layout, navigationHeight, actualNavigationHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
}
}
shareImpl = { [weak self] in
self?.requestMultipleAction?()
}
}
deinit {
self.presentationDataDisposable?.dispose()
}
func beginSelection() {
self.contactListNode.updateSelectionState({ _ in
return ContactListNodeGroupSelectionState()
})
}
private func updateTheme() {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.searchDisplayController?.updatePresentationData(presentationData)
@ -89,7 +125,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight)
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
@ -107,13 +143,21 @@ final class ContactSelectionControllerNode: ASDisplayNode {
self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size)
let countPanelHeight = self.countPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
if (self.selectionState?.selectedPeerIndices.isEmpty ?? true) {
transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: countPanelHeight)))
} else {
insets.bottom += countPanelHeight
transition.updateFrame(node: self.countPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - countPanelHeight), size: CGSize(width: layout.size.width, height: countPanelHeight)))
}
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
return
}
@ -124,7 +168,35 @@ final class ContactSelectionControllerNode: ASDisplayNode {
categories.insert(.global)
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in
self?.requestOpenPeerFromSearch?(peer)
if let strongSelf = self {
var updated = false
strongSelf.contactListNode.updateSelectionState { state -> ContactListNodeGroupSelectionState? in
if let state = state {
updated = true
var foundPeers = state.foundPeers
var selectedPeerMap = state.selectedPeerMap
selectedPeerMap[peer.id] = peer
var exists = false
for foundPeer in foundPeers {
if peer.id == foundPeer.id {
exists = true
break
}
}
if !exists {
foundPeers.insert(peer, at: 0)
}
return state.withToggledPeerId(peer.id).withFoundPeers(foundPeers).withSelectedPeerMap(selectedPeerMap)
} else {
return nil
}
}
if updated {
strongSelf.requestDeactivateSearch?()
} else {
strongSelf.requestOpenPeerFromSearch?(peer)
}
}
}, contextAction: nil), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()
@ -169,3 +241,123 @@ final class ContactSelectionControllerNode: ASDisplayNode {
})
}
}
final class ContactSelectionCountPanelNode: ASDisplayNode {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let separatorNode: ASDisplayNode
private let button: HighlightTrackingButtonNode
private let badgeLabel: TextNode
private var badgeText: NSAttributedString?
private let badgeBackground: ASImageNode
private let action: (() -> Void)
private var validLayout: (CGFloat, CGFloat, CGFloat)?
var count: Int = 0 {
didSet {
if self.count != oldValue && self.count > 0 {
self.badgeText = NSAttributedString(string: "\(count)", font: Font.regular(14.0), textColor: self.theme.actionSheet.opaqueItemBackgroundColor, paragraphAlignment: .center)
self.badgeLabel.isHidden = false
self.badgeBackground.isHidden = false
if let (width, sideInset, bottomInset) = self.validLayout {
let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate)
}
}
}
}
init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.action = action
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
self.badgeLabel = TextNode()
self.badgeLabel.isHidden = true
self.badgeLabel.isUserInteractionEnabled = false
self.badgeLabel.displaysAsynchronously = false
self.badgeBackground = ASImageNode()
self.badgeBackground.isHidden = true
self.badgeBackground.isLayerBacked = true
self.badgeBackground.displaysAsynchronously = false
self.badgeBackground.displayWithoutProcessing = true
self.badgeBackground.image = generateStretchableFilledCircleImage(diameter: 22.0, color: theme.actionSheet.controlAccentColor)
self.button = HighlightTrackingButtonNode()
self.button.setTitle(strings.ShareMenu_Send, with: Font.medium(17.0), with: theme.actionSheet.controlAccentColor, for: .normal)
super.init()
self.backgroundColor = theme.rootController.navigationBar.backgroundColor
self.addSubnode(self.badgeBackground)
self.addSubnode(self.badgeLabel)
self.addSubnode(self.button)
self.addSubnode(self.separatorNode)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.badgeBackground.layer.removeAnimation(forKey: "opacity")
strongSelf.badgeBackground.alpha = 0.4
strongSelf.badgeLabel.layer.removeAnimation(forKey: "opacity")
strongSelf.badgeLabel.alpha = 0.4
strongSelf.button.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.button.titleNode.alpha = 0.4
} else {
strongSelf.badgeBackground.alpha = 1.0
strongSelf.badgeBackground.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.badgeLabel.alpha = 1.0
strongSelf.badgeLabel.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.button.titleNode.alpha = 1.0
strongSelf.button.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.button.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.action()
}
func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = (width, sideInset, bottomInset)
let topInset: CGFloat = 9.0
var bottomInset = bottomInset
bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0)
let height = 44.0 + bottomInset
self.button.frame = CGRect(x: sideInset, y: 0.0, width: width - sideInset * 2.0, height: 44.0)
if !self.badgeLabel.isHidden {
let (badgeLayout, badgeApply) = TextNode.asyncLayout(self.badgeLabel)(TextNodeLayoutArguments(attributedString: self.badgeText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let _ = badgeApply()
let backgroundSize = CGSize(width: max(22.0, badgeLayout.size.width + 10.0 + 1.0), height: 22.0)
let backgroundFrame = CGRect(origin: CGPoint(x: self.button.titleNode.frame.maxX + 6.0, y: self.button.bounds.size.height - 33.0), size: backgroundSize)
self.badgeBackground.frame = backgroundFrame
self.badgeLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeLayout.size.width / 2.0), y: backgroundFrame.minY + 3.0), size: badgeLayout.size)
}
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
return height
}
}

View File

@ -146,6 +146,7 @@ public final class NotificationViewControllerImpl {
return nil
}, requestSetAlternateIconName: { _, f in
f(false)
}, forceOrientation: { _ in
})
let presentationDataPromise = Promise<PresentationData>()

View File

@ -910,7 +910,7 @@ private enum PlayPauseIconNodeState: Equatable {
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .pause
init() {

View File

@ -6969,8 +6969,8 @@ func presentAddMembers(context: AccountContext, parentController: ViewController
parentController?.push(contactsController)
if let contactsController = contactsController as? ContactSelectionController {
selectAddMemberDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak contactsController] memberPeer in
guard let (memberPeer, _) = memberPeer else {
|> deliverOnMainQueue).start(next: { [weak contactsController] result in
guard let (peers, _) = result, let memberPeer = peers.first else {
return
}

View File

@ -198,6 +198,7 @@ public class ShareRootControllerImpl {
return nil
}, requestSetAlternateIconName: { _, f in
f(false)
}, forceOrientation: { _ in
})
let internalContext: InternalContext