Added animated sticker nodes in context suggestion panel & trending

This commit is contained in:
Ilya Laktyushin
2019-07-01 18:54:07 +02:00
parent 4c707772ce
commit 6fe3789f8d
8 changed files with 199 additions and 88 deletions

View File

@@ -29,7 +29,7 @@ public enum ChannelOwnershipTransferError {
public func checkOwnershipTranfserAvailability(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, memberId: PeerId) -> Signal<Never, ChannelOwnershipTransferError> {
return postbox.transaction { transaction -> Peer? in
return transaction.getPeer(memberId)
}
}
|> introduceError(ChannelOwnershipTransferError.self)
|> mapToSignal { user -> Signal<Never, ChannelOwnershipTransferError> in
guard let user = user else {

View File

@@ -20,7 +20,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var shareButtonNode: HighlightableButtonNode?
var telegramFile: TelegramMediaFile?
private let disposable = MetaDisposable()
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private var replyInfoNode: ChatMessageReplyInfoNode?
@@ -46,11 +45,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.addSubnode(self.animationNode)
self.addSubnode(self.dateAndStatusNode)
}
deinit {
self.disposable.dispose()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

View File

@@ -309,13 +309,30 @@ public class ContactsController: ViewController {
}
self.contactsNode.openInvite = { [weak self] in
if let strongSelf = self {
(strongSelf.navigationController as? NavigationController)?.pushViewController(InviteContactsController(context: strongSelf.context), completion: {
if let strongSelf = self {
let _ = (DeviceAccess.authorizationStatus(subject: .contacts)
|> take(1)
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
switch value {
case .allowed:
(strongSelf.navigationController as? NavigationController)?.pushViewController(InviteContactsController(context: strongSelf.context), completion: {
if let strongSelf = self {
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}
})
case .notDetermined:
DeviceAccess.authorizeAccess(to: .contacts)
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}
})
}
default:
let presentationData = strongSelf.presentationData
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
self?.context.sharedContext.applicationBindings.openSettings()
})]), in: .window(.root))
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
}
})
}
self.contactsNode.contactListNode.openSortMenu = { [weak self] in

View File

@@ -83,25 +83,9 @@ final class ContactsControllerNode: ASDisplayNode {
}
inviteImpl = { [weak self] in
let _ = (DeviceAccess.authorizationStatus(subject: .contacts)
|> take(1)
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
switch value {
case .allowed:
strongSelf.openInvite?()
case .notDetermined:
DeviceAccess.authorizeAccess(to: .contacts)
default:
let presentationData = strongSelf.presentationData
present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
self?.context.sharedContext.applicationBindings.openSettings()
})]), nil)
}
})
if let strongSelf = self {
strongSelf.openInvite?()
}
}
}

View File

@@ -155,7 +155,7 @@ class ContactsPeerItem: ListViewItem {
self.deletePeer = deletePeer
self.header = header
self.itemHighlighting = itemHighlighting
self.selectable = self.enabled
self.selectable = enabled
if let index = index {
var letter: String = "#"
@@ -369,33 +369,14 @@ class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
if let item = self.item, case .selectable = item.selection {
return
}
super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
// if highlighted && self.selectionNode == nil {
// self.highlightedBackgroundNode.alpha = 1.0
// if self.highlightedBackgroundNode.supernode == nil {
// self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
// }
// } else {
// if self.highlightedBackgroundNode.supernode != nil {
// if animated {
// self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
// if let strongSelf = self {
// if completed {
// strongSelf.highlightedBackgroundNode.removeFromSupernode()
// }
// }
// })
// self.highlightedBackgroundNode.alpha = 0.0
// } else {
// self.highlightedBackgroundNode.removeFromSupernode()
// }
// }
// }
}

View File

@@ -41,6 +41,7 @@ final class HorizontalStickerGridItem: GridItem {
final class HorizontalStickerGridItemNode: GridItemNode {
private var currentState: (Account, HorizontalStickerGridItem, CGSize)?
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private let stickerFetchedDisposable = MetaDisposable()
@@ -48,6 +49,18 @@ final class HorizontalStickerGridItemNode: GridItemNode {
private var currentIsPreviewing: Bool = false
override var isVisibleInGrid: Bool {
didSet {
if oldValue != self.isVisibleInGrid {
if self.isVisibleInGrid {
self.animationNode?.visibility = true
} else {
self.animationNode?.visibility = false
}
}
}
}
var stickerItem: StickerPackItem? {
if let (_, item, _) = self.currentState {
return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.file, indexKeys: [])
@@ -66,7 +79,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
}
deinit {
stickerFetchedDisposable.dispose()
self.stickerFetchedDisposable.dispose()
}
override func didLoad() {
@@ -78,8 +91,33 @@ final class HorizontalStickerGridItemNode: GridItemNode {
func setup(account: Account, item: HorizontalStickerGridItem) {
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1.file.id != item.file.id {
if let dimensions = item.file.dimensions {
self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start())
if item.file.isAnimatedSticker {
self.stickerFetchedDisposable.set(nil)
let animationNode: AnimatedStickerNode
if let currentAnimationNode = self.animationNode {
animationNode = currentAnimationNode
} else {
animationNode = AnimatedStickerNode()
animationNode.transform = self.imageNode.transform
animationNode.visibility = self.isVisibleInGrid
self.addSubnode(animationNode)
self.animationNode = animationNode
}
animationNode.started = { [weak self] in
self?.imageNode.alpha = 0.0
}
animationNode.setup(account: account, fileReference: stickerPackFileReference(item.file), width: 140, height: 140)
} else {
self.imageNode.alpha = 1.0
self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start())
if let currentAnimationNode = self.animationNode {
self.animationNode = nil
currentAnimationNode.removeFromSupernode()
}
}
self.currentState = (account, item, dimensions)
self.setNeedsLayout()
@@ -101,6 +139,12 @@ final class HorizontalStickerGridItemNode: GridItemNode {
let imageFrame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: CGSize(width: imageSize.width, height: imageSize.height))
self.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: imageSize.width, height: imageSize.height))
self.imageNode.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY)
if let animationNode = self.animationNode {
animationNode.bounds = self.imageNode.bounds
animationNode.position = self.imageNode.position
animationNode.updateLayout(size: self.imageNode.bounds.size)
}
}
}

View File

@@ -204,7 +204,7 @@ struct InviteContactsGroupSelectionState: Equatable {
}
}
private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)], selectionState: InviteContactsGroupSelectionState, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: InviteContactsInteraction) -> [InviteContactsEntry] {
private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?, selectionState: InviteContactsGroupSelectionState, theme: PresentationTheme, strings: PresentationStrings, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: InviteContactsInteraction) -> [InviteContactsEntry] {
var entries: [InviteContactsEntry] = []
entries.append(.option(0, ContactListAdditionalOption(title: strings.Contacts_ShareTelegram, icon: .generic(UIImage(bundleImageName: "Contact List/InviteActionIcon")!), action: {
@@ -212,35 +212,39 @@ private func inviteContactsEntries(accountPeer: Peer?, sortedContacts: [(DeviceC
}), theme, strings))
var index = 0
for (id, contact, count) in sortedContacts {
entries.append(.peer(index, id, contact, count, .selectable(selected: selectionState.selectedContactIndices[id] != nil), theme, strings, nameSortOrder, nameDisplayOrder))
index += 1
if let sortedContacts = sortedContacts {
for (id, contact, count) in sortedContacts {
entries.append(.peer(index, id, contact, count, .selectable(selected: selectionState.selectedContactIndices[id] != nil), theme, strings, nameSortOrder, nameDisplayOrder))
index += 1
}
}
return entries
}
private func preparedInviteContactsTransition(account: Account, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContats: [(DeviceContactStableId, DeviceContactBasicData, Int32)], interaction: InviteContactsInteraction, firstTime: Bool, animated: Bool) -> InviteContactsTransition {
private func preparedInviteContactsTransition(account: Account, from fromEntries: [InviteContactsEntry], to toEntries: [InviteContactsEntry], sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?, interaction: InviteContactsInteraction, firstTime: Bool, isLoading: Bool, animated: Bool) -> InviteContactsTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction), directionHint: nil) }
return InviteContactsTransition(deletions: deletions, insertions: insertions, updates: updates, sortedContats: sortedContats, firstTime: firstTime, animated: animated)
return InviteContactsTransition(deletions: deletions, insertions: insertions, updates: updates, sortedContacts: sortedContacts, firstTime: firstTime, isLoading: isLoading, animated: animated)
}
private struct InviteContactsTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let sortedContats: [(DeviceContactStableId, DeviceContactBasicData, Int32)]
let sortedContacts: [(DeviceContactStableId, DeviceContactBasicData, Int32)]?
let firstTime: Bool
let isLoading: Bool
let animated: Bool
}
final class InviteContactsControllerNode: ASDisplayNode {
let listNode: ListView
private var activityIndicator: ActivityIndicator?
private let context: AccountContext
private var searchDisplayController: SearchDisplayController?
@@ -365,8 +369,8 @@ final class InviteContactsControllerNode: ASDisplayNode {
}
let currentSortedContacts = self.currentSortedContacts
let sortedContacts: Signal<[(DeviceContactStableId, DeviceContactBasicData, Int32)], NoError> = combineLatest(existingNumbers, (context.sharedContext.contactDataManager?.basicData() ?? .single([:])) |> take(1))
|> mapToSignal { existingNumbersAndPeerIds, contacts -> Signal<[(DeviceContactStableId, DeviceContactBasicData, Int32)], NoError> in
let sortedContacts: Signal<[(DeviceContactStableId, DeviceContactBasicData, Int32)]?, NoError> = combineLatest(existingNumbers, (context.sharedContext.contactDataManager?.basicData() ?? .single([:])) |> take(1))
|> mapToSignal { existingNumbersAndPeerIds, contacts -> Signal<[(DeviceContactStableId, DeviceContactBasicData, Int32)]?, NoError> in
var mappedContacts: [(String, [DeviceContactNormalizedPhoneNumber])] = []
for (id, basicData) in contacts {
mappedContacts.append((id: id, basicData.phoneNumbers.map({ phoneNumber in
@@ -374,7 +378,7 @@ final class InviteContactsControllerNode: ASDisplayNode {
})))
}
return deviceContactsImportedByCount(postbox: context.account.postbox, contacts: mappedContacts)
|> map { counts -> [(DeviceContactStableId, DeviceContactBasicData, Int32)] in
|> map { counts -> [(DeviceContactStableId, DeviceContactBasicData, Int32)]? in
var result: [(DeviceContactStableId, DeviceContactBasicData, Int32)] = []
var contactValues: [DeviceContactStableId: DeviceContactBasicData] = [:]
for (id, basicData) in contacts {
@@ -410,10 +414,13 @@ final class InviteContactsControllerNode: ASDisplayNode {
}
}
|> beforeNext { sortedContacts in
let _ = currentSortedContacts.swap(sortedContacts)
if let sortedContacts = sortedContacts {
let _ = currentSortedContacts.swap(sortedContacts)
}
}
let processingQueue = Queue()
transition = (combineLatest(sortedContacts, selectionStateSignal, themeAndStringsPromise.get())
transition = (combineLatest(.single(nil) |> then(sortedContacts), selectionStateSignal, themeAndStringsPromise.get())
|> mapToQueue { sortedContacts, selectionState, themeAndStrings -> Signal<InviteContactsTransition, NoError> in
let signal = deferred { () -> Signal<InviteContactsTransition, NoError> in
let entries = inviteContactsEntries(accountPeer: nil, sortedContacts: sortedContacts, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1, nameSortOrder: themeAndStrings.2, nameDisplayOrder: themeAndStrings.3, interaction: interaction)
@@ -424,16 +431,18 @@ final class InviteContactsControllerNode: ASDisplayNode {
} else {
animated = false
}
return .single(preparedInviteContactsTransition(account: context.account, from: previous ?? [], to: entries, sortedContats: sortedContacts, interaction: interaction, firstTime: previous == nil, animated: animated))
return .single(preparedInviteContactsTransition(account: context.account, from: previous ?? [], to: entries, sortedContacts: sortedContacts, interaction: interaction, firstTime: previous == nil, isLoading: false, animated: animated))
}
return signal
|> runOn(processingQueue)
if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
return signal
|> runOn(Queue.mainQueue())
} else {
return signal
|> runOn(processingQueue)
}
// if OSAtomicCompareAndSwap32(1, 0, &firstTime) {
// return signal
// |> runOn(Queue.mainQueue())
// } else {
// return signal
// |> runOn(processingQueue)
// }
})
|> deliverOnMainQueue
@@ -441,6 +450,8 @@ final class InviteContactsControllerNode: ASDisplayNode {
self?.enqueueTransition(transition)
})
self.enqueueTransition(InviteContactsTransition(deletions: [], insertions: [], updates: [], sortedContacts: [], firstTime: true, isLoading: true, animated: false))
shareImpl = { [weak self] in
if let strongSelf = self {
var result: [(DeviceContactBasicData, Int32)] = []
@@ -524,6 +535,11 @@ final class InviteContactsControllerNode: ASDisplayNode {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if let activityIndicator = self.activityIndicator {
let indicatorSize = activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: updateSizeAndInsets.insets.top + 50.0 + floor((layout.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
}
if !hadValidLayout {
self.dequeueTransitions()
}
@@ -587,6 +603,15 @@ final class InviteContactsControllerNode: ASDisplayNode {
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.readyValue = true
if transition.isLoading, strongSelf.activityIndicator == nil {
let activityIndicator = ActivityIndicator(type: .custom(strongSelf.presentationData.theme.list.itemAccentColor, 22.0, 1.0, false))
strongSelf.activityIndicator = activityIndicator
strongSelf.insertSubnode(activityIndicator, aboveSubnode: strongSelf.listNode)
} else if !transition.isLoading, let activityIndicator = strongSelf.activityIndicator {
strongSelf.activityIndicator = nil
activityIndicator.removeFromSupernode()
}
}
})
}
@@ -605,4 +630,3 @@ final class InviteContactsControllerNode: ASDisplayNode {
self.selectionState = self.selectionState.withReplacedSelectedContactIds(allSelected ? [] : ids)
}
}

View File

@@ -66,12 +66,63 @@ private let titleFont = Font.bold(16.0)
private let statusFont = Font.regular(15.0)
private let buttonFont = Font.medium(13.0)
private final class TrendingTopItemNode: TransformImageNode {
var file: TelegramMediaFile? = nil
let loadDisposable = MetaDisposable()
private final class TrendingTopItemNode: ASDisplayNode {
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
public private(set) var file: TelegramMediaFile? = nil
private var itemSize: CGSize?
private let loadDisposable = MetaDisposable()
var currentIsPreviewing = false
var visibility: Bool = false {
didSet {
if oldValue != self.visibility {
self.animationNode?.visibility = self.visibility
}
}
}
override init() {
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
super.init()
self.addSubnode(self.imageNode)
}
func setup(account: Account, item: StickerPackItem, itemSize: CGSize, synchronousLoads: Bool) {
self.file = item.file
self.itemSize = itemSize
if item.file.isAnimatedSticker {
self.loadDisposable.set(nil)
let animationNode: AnimatedStickerNode
if let currentAnimationNode = self.animationNode {
animationNode = currentAnimationNode
} else {
animationNode = AnimatedStickerNode()
animationNode.transform = self.imageNode.transform
animationNode.visibility = self.visibility
self.addSubnode(animationNode)
self.animationNode = animationNode
}
animationNode.started = { [weak self] in
self?.imageNode.alpha = 0.0
}
animationNode.setup(account: account, fileReference: stickerPackFileReference(item.file), width: 140, height: 140)
} else {
self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start())
if let currentAnimationNode = self.animationNode {
self.animationNode = nil
currentAnimationNode.removeFromSupernode()
}
}
}
func updatePreviewing(animated: Bool, isPreviewing: Bool) {
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing
@@ -88,6 +139,18 @@ private final class TrendingTopItemNode: TransformImageNode {
}
}
}
override func layout() {
super.layout()
if let dimensions = self.file?.dimensions, let itemSize = self.itemSize {
let imageSize = dimensions.aspectFitted(itemSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
}
self.imageNode.frame = self.bounds
self.animationNode?.updateLayout(size: self.bounds.size)
}
}
class MediaInputPaneTrendingItemNode: ListViewItemNode {
@@ -109,6 +172,10 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode {
let isVisible = self.visibility != .none
if isVisible != wasVisible {
for node in self.itemNodes {
node.visibility = isVisible
}
if isVisible {
if let item = self.item, item.unread {
self.readDisposable.set((
@@ -278,6 +345,8 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode {
var offset = sideInset
let itemSpacing = (max(0, availableWidth - 5.0 * itemSide - sideInset * 2.0)) / 4.0
let isVisible = strongSelf.visibility != .none
for i in 0 ..< topItems.count {
let file = topItems[i].file
let node: TrendingTopItemNode
@@ -285,18 +354,15 @@ class MediaInputPaneTrendingItemNode: ListViewItemNode {
node = strongSelf.itemNodes[i]
} else {
node = TrendingTopItemNode()
node.contentAnimations = [.subsequentUpdates]
node.visibility = isVisible
strongSelf.itemNodes.append(node)
strongSelf.addSubnode(node)
}
if file.fileId != node.file?.fileId {
node.file = file
node.setSignal(chatMessageSticker(account: item.account, file: file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
node.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start())
node.setup(account: item.account, item: topItems[i], itemSize: itemSize, synchronousLoads: synchronousLoads)
}
if let dimensions = file.dimensions {
let imageSize = dimensions.aspectFitted(itemSize)
node.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0), size: imageSize)
offset += itemSize.width + itemSpacing
}