mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various Improvements
This commit is contained in:
parent
cd6af7d537
commit
498dd0bbec
Binary file not shown.
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ swift_library(
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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, _, _):
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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: [
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "msg_maxvideo.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/msg_maxvideo.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Media Gallery/Fullscreen.imageset/msg_maxvideo.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "msg_minvideo.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/msg_minvideo.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Media Gallery/Minimize.imageset/msg_minvideo.pdf
vendored
Normal file
Binary file not shown.
@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "soundoff (2).pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "soundon (2).pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ public final class NotificationViewControllerImpl {
|
||||
return nil
|
||||
}, requestSetAlternateIconName: { _, f in
|
||||
f(false)
|
||||
}, forceOrientation: { _ in
|
||||
})
|
||||
|
||||
let presentationDataPromise = Promise<PresentationData>()
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -198,6 +198,7 @@ public class ShareRootControllerImpl {
|
||||
return nil
|
||||
}, requestSetAlternateIconName: { _, f in
|
||||
f(false)
|
||||
}, forceOrientation: { _ in
|
||||
})
|
||||
|
||||
let internalContext: InternalContext
|
||||
|
Loading…
x
Reference in New Issue
Block a user