import Foundation import UIKit import Display import QuickLook import SwiftSignalKit import AsyncDisplayKit import TelegramCore import TelegramPresentationData import AccountContext import GalleryUI import LegacyComponents import LegacyMediaPickerUI import SaveToCameraRoll import OverlayStatusController import PresentationDataUtils public enum AvatarGalleryEntryId: Hashable { case topImage case image(EngineMedia.Id) case resource(String) } public func peerInfoProfilePhotos(context: AccountContext, peerId: EnginePeer.Id) -> Signal { return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> mapToSignal { peer -> Signal<[AvatarGalleryEntry]?, NoError> in guard let peer = peer else { return .single(nil) } return initialAvatarGalleryEntries(account: context.account, engine: context.engine, peer: peer) } |> distinctUntilChanged |> mapToSignal { entries -> Signal<(Bool, [AvatarGalleryEntry])?, NoError> in if let entries = entries { if var firstEntry = entries.first { return context.account.postbox.peerView(id: peerId) |> mapToSignal { peerView -> Signal<(Bool, [AvatarGalleryEntry])?, NoError>in if let peer = peerViewMainPeer(peerView) { var secondEntry: TelegramMediaImage? var lastEntry: TelegramMediaImage? if let cachedData = peerView.cachedData as? CachedUserData { if let firstRepresentation = firstEntry.representations.first, firstRepresentation.representation.isPersonal { if firstRepresentation.representation.hasVideo, case let .known(photo) = cachedData.personalPhoto, let peerReference = PeerReference(peer) { firstEntry = .topImage(firstEntry.representations, photo?.videoRepresentations.map { VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) } ?? [], firstEntry.peer, firstEntry.indexData, firstEntry.immediateThumbnailData, nil) } if case let .known(photo) = cachedData.photo { secondEntry = photo } } if case let .known(photo) = cachedData.fallbackPhoto { lastEntry = photo } } return fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: EnginePeer(peer), firstEntry: firstEntry, secondEntry: secondEntry, lastEntry: lastEntry) |> map(Optional.init) } else { return .single(nil) } } } else { return .single((true, [])) } } else { return context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId) |> map { _ -> (Bool, [AvatarGalleryEntry])? in return nil } } } |> map { items -> Any in if let items = items { return items } else { return peerInfoProfilePhotos(context: context, peerId: peerId) } } } public func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: EnginePeer.Id) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) |> map { items -> (Bool, [AvatarGalleryEntry]) in return items as? (Bool, [AvatarGalleryEntry]) ?? (true, []) } } public enum AvatarGalleryEntry: Equatable { case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], EnginePeer?, GalleryItemIndexData?, Data?, String?) case image(EngineMedia.Id, TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], EnginePeer?, Int32?, GalleryItemIndexData?, EngineMessage.Id?, Data?, String?, Bool, TelegramMediaImage.EmojiMarkup?) public init(representation: TelegramMediaImageRepresentation, peer: EnginePeer) { self = .topImage([ImageRepresentationWithReference(representation: representation, reference: MediaResourceReference.standalone(resource: representation.resource))], [], peer, nil, nil, nil) } public var id: AvatarGalleryEntryId { switch self { case let .topImage(representations, _, _, _, _, _): if let last = representations.last { return .resource(last.representation.resource.id.stringRepresentation) } return .topImage case let .image(id, _, representations, _, _, _, _, _, _, _, _, _): if let last = representations.last { return .resource(last.representation.resource.id.stringRepresentation) } return .image(id) } } public var peer: EnginePeer? { switch self { case let .topImage(_, _, peer, _, _, _): return peer case let .image(_, _, _, _, peer, _, _, _, _, _, _, _): return peer } } public var representations: [ImageRepresentationWithReference] { switch self { case let .topImage(representations, _, _, _, _, _): return representations case let .image(_, _, representations, _, _, _, _, _, _, _, _, _): return representations } } public var immediateThumbnailData: Data? { switch self { case let .topImage(_, _, _, _, immediateThumbnailData, _): return immediateThumbnailData case let .image(_, _, _, _, _, _, _, _, immediateThumbnailData, _, _, _): return immediateThumbnailData } } public var videoRepresentations: [VideoRepresentationWithReference] { switch self { case let .topImage(_, videoRepresentations, _, _, _, _): return videoRepresentations case let .image(_, _, _, videoRepresentations, _, _, _, _, _, _, _, _): return videoRepresentations } } public var emojiMarkup: TelegramMediaImage.EmojiMarkup? { switch self { case .topImage: return nil case let .image(_, _, _, _, _, _, _, _, _, _, _, markup): return markup } } public var indexData: GalleryItemIndexData? { switch self { case let .topImage(_, _, _, indexData, _, _): return indexData case let .image(_, _, _, _, _, _, indexData, _, _, _, _, _): return indexData } } public static func ==(lhs: AvatarGalleryEntry, rhs: AvatarGalleryEntry) -> Bool { switch lhs { case let .topImage(lhsRepresentations, lhsVideoRepresentations, lhsPeer, lhsIndexData, lhsImmediateThumbnailData, lhsCategory): if case let .topImage(rhsRepresentations, rhsVideoRepresentations, rhsPeer, rhsIndexData, rhsImmediateThumbnailData, rhsCategory) = rhs, lhsRepresentations == rhsRepresentations, lhsVideoRepresentations == rhsVideoRepresentations, lhsPeer == rhsPeer, lhsIndexData == rhsIndexData, lhsImmediateThumbnailData == rhsImmediateThumbnailData, lhsCategory == rhsCategory { return true } else { return false } case let .image(lhsId, lhsImageReference, lhsRepresentations, lhsVideoRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId, lhsImmediateThumbnailData, lhsCategory, lhsIsFallback, lhsEmojiMarkup): if case let .image(rhsId, rhsImageReference, rhsRepresentations, rhsVideoRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId, rhsImmediateThumbnailData, rhsCategory, rhsIsFallback, rhsEmojiMarkup) = rhs, lhsId == rhsId, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, lhsVideoRepresentations == rhsVideoRepresentations, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId, lhsImmediateThumbnailData == rhsImmediateThumbnailData, lhsCategory == rhsCategory, lhsIsFallback == rhsIsFallback, lhsEmojiMarkup == rhsEmojiMarkup { return true } else { return false } } } } public final class AvatarGalleryControllerPresentationArguments { let animated: Bool let transitionArguments: (AvatarGalleryEntry) -> GalleryTransitionArguments? public init(animated: Bool = true, transitionArguments: @escaping (AvatarGalleryEntry) -> GalleryTransitionArguments?) { self.animated = animated self.transitionArguments = transitionArguments } } public func normalizeEntries(_ entries: [AvatarGalleryEntry]) -> [AvatarGalleryEntry] { var updatedEntries: [AvatarGalleryEntry] = [] let count: Int32 = Int32(entries.count) var index: Int32 = 0 for entry in entries { let indexData = GalleryItemIndexData(position: index, totalCount: count) if case let .topImage(representations, videoRepresentations, peer, _, immediateThumbnailData, category) = entry { updatedEntries.append(.topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category)) } else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category, isFallback, emojiMarkup) = entry { updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category, isFallback, emojiMarkup)) } index += 1 } return updatedEntries } public func initialAvatarGalleryEntries(account: Account, engine: TelegramEngine, peer: EnginePeer) -> Signal<[AvatarGalleryEntry]?, NoError> { var initialEntries: [AvatarGalleryEntry] = [] if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer._asPeer()) { initialEntries.append(.topImage(peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), [], peer, nil, nil, nil)) } guard let peerReference = PeerReference(peer._asPeer()) else { return .single(initialEntries) } switch peer { case .channel, .legacyGroup: break default: return .single(initialEntries) } return engine.data.get(TelegramEngine.EngineData.Item.Peer.Photo(id: peer.id)) |> map { peerPhoto in var initialPhoto: TelegramMediaImage? if case let .known(value) = peerPhoto { initialPhoto = value } if let photo = initialPhoto { var representations = photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }) if photo.immediateThumbnailData == nil, let firstEntry = initialEntries.first, let firstRepresentation = firstEntry.representations.first { representations.insert(firstRepresentation, at: 0) } return [.image(photo.imageId, photo.reference, representations, photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, nil, nil, nil, photo.immediateThumbnailData, nil, false, photo.emojiMarkup)] } else { if case .known = peerPhoto { return [] } else { return nil } } } } public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account, peer: EnginePeer) -> Signal<[AvatarGalleryEntry], NoError> { return initialAvatarGalleryEntries(account: account, engine: engine, peer: peer) |> map { entries -> [AvatarGalleryEntry] in return entries ?? [] } |> mapToSignal { initialEntries in return .single(initialEntries) |> then( engine.peers.requestPeerPhotos(peerId: peer.id) |> map { photos -> [AvatarGalleryEntry] in var result: [AvatarGalleryEntry] = [] if photos.isEmpty { result = initialEntries } else if let peerReference = PeerReference(peer._asPeer()) { var index: Int32 = 0 if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) { var initialMediaIds = Set() for entry in initialEntries { if case let .image(mediaId, _, _, _, _, _, _, _, _, _, _, _) = entry { initialMediaIds.insert(mediaId) } } var photosCount = photos.count for i in 0 ..< photos.count { let photo = photos[i] if i == 0 && !initialMediaIds.contains(photo.image.imageId) { photosCount += 1 for entry in initialEntries { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) if case let .image(mediaId, imageReference, representations, videoRepresentations, peer, _, _, _, thumbnailData, _, _, emojiMarkup) = entry { result.append(.image(mediaId, imageReference, representations, videoRepresentations, peer, nil, indexData, nil, thumbnailData, nil, false, emojiMarkup)) index += 1 } } } let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false, photo.image.emojiMarkup)) index += 1 } } else { for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { result.append(.image(photo.image.imageId, photo.image.reference, first.representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false, photo.image.emojiMarkup)) } else { result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false, photo.image.emojiMarkup)) } index += 1 } } } return result } ) } } public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account, peer: EnginePeer, firstEntry: AvatarGalleryEntry, secondEntry: TelegramMediaImage?, lastEntry: TelegramMediaImage?) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { let initialEntries = [firstEntry] return Signal<(Bool, [AvatarGalleryEntry]), NoError>.single((false, initialEntries)) |> then( engine.peers.requestPeerPhotos(peerId: peer.id) |> map { photos -> (Bool, [AvatarGalleryEntry]) in var result: [AvatarGalleryEntry] = [] let initialEntries = [firstEntry] if photos.isEmpty { result = initialEntries } else if let peerReference = PeerReference(peer._asPeer()) { var index: Int32 = 0 if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) { var initialMediaIds = Set() for entry in initialEntries { if case let .image(mediaId, _, _, _, _, _, _, _, _, _, _, _) = entry { initialMediaIds.insert(mediaId) } } var photosCount = photos.count for i in 0 ..< photos.count { let photo = photos[i] var representations = photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) if i == 0 { if !initialMediaIds.contains(photo.image.imageId) { photosCount += 1 for entry in initialEntries { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) if case let .image(mediaId, imageReference, representations, videoRepresentations, peer, _, _, _, thumbnailData, _, _, emojiMarkup) = entry { result.append(.image(mediaId, imageReference, representations, videoRepresentations, peer, nil, indexData, nil, thumbnailData, nil, false, emojiMarkup)) index += 1 } } } else if photo.image.immediateThumbnailData == nil, let firstEntry = initialEntries.first, let firstRepresentation = firstEntry.representations.first { representations.insert(firstRepresentation, at: 0) } } let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) result.append(.image(photo.image.imageId, photo.image.reference, representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false, photo.image.emojiMarkup)) index += 1 } } else { var photos = photos if let secondEntry { photos.insert(TelegramPeerPhoto(image: secondEntry, reference: secondEntry.reference, date: photos.first?.date ?? 0, index: 1, totalCount: 0, messageId: nil), at: 1) } if let lastEntry { photos.append(TelegramPeerPhoto(image: lastEntry, reference: lastEntry.reference, date: 0, index: photos.count, totalCount: 0, messageId: nil)) } for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { var videoRepresentations: [VideoRepresentationWithReference] = first.videoRepresentations var emojiMarkup: TelegramMediaImage.EmojiMarkup? = first.emojiMarkup let isPersonal = first.representations.first?.representation.isPersonal == true if videoRepresentations.isEmpty, !isPersonal { videoRepresentations = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) } if emojiMarkup == nil, !isPersonal { emojiMarkup = photo.image.emojiMarkup } result.append(.image(photo.image.imageId, photo.image.reference, first.representations, videoRepresentations, peer, secondEntry != nil ? 0 : photo.date, indexData, photo.messageId, first.immediateThumbnailData ?? photo.image.immediateThumbnailData, nil, false, emojiMarkup)) } else { result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, photo.image.id == lastEntry?.id, photo.image.emojiMarkup)) } index += 1 } } } return (true, result) } ) } public class AvatarGalleryController: ViewController, StandalonePresentableController { public enum SourceCorners { case none case round case roundRect(CGFloat) } private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode } private let context: AccountContext private let peer: EnginePeer private let sourceCorners: SourceCorners private let isSuggested: Bool private var presentationData: PresentationData private let _ready = Promise() private let animatedIn = ValuePromise(true) override public var ready: Promise { return self._ready } private var didSetReady = false private var adjustedForInitialPreviewingLayout = false private let disposable = MetaDisposable() private var entries: [AvatarGalleryEntry] = [] private var centralEntryIndex: Int? private let centralItemTitle = Promise() private let centralItemTitleView = Promise() private let centralItemRightBarButtonItems = Promise<[UIBarButtonItem]?>(nil) private let centralItemNavigationStyle = Promise() private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() private let centralItemAttributesDisposable = DisposableSet(); public var openAvatarSetup: ((@escaping () -> Void) -> Void)? public var removedEntry: ((AvatarGalleryEntry) -> Void)? private let _hiddenMedia = Promise(nil) public var hiddenMedia: Signal { return self._hiddenMedia.get() } private let replaceRootController: (ViewController, Promise?) -> Void private let editDisposable = MetaDisposable () public init(context: AccountContext, peer: EnginePeer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, isSuggested: Bool = false, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise?) -> Void, synchronousLoad: Bool = false) { self.context = context self.peer = peer self.sourceCorners = sourceCorners self.isSuggested = isSuggested self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController self.centralEntryIndex = centralEntryIndex super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed)) self.navigationItem.leftBarButtonItem = backItem self.statusBar.statusBarStyle = .White let remoteEntriesSignal: Signal<[AvatarGalleryEntry], NoError> if let remoteEntries = remoteEntries { remoteEntriesSignal = remoteEntries.get() } else { remoteEntriesSignal = fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: peer) } let initialSignal = initialAvatarGalleryEntries(account: context.account, engine: context.engine, peer: peer) |> map { entries -> [AvatarGalleryEntry] in return entries ?? [] } let entriesSignal: Signal<[AvatarGalleryEntry], NoError> = skipInitial ? remoteEntriesSignal : (initialSignal |> then(remoteEntriesSignal)) let presentationData = self.presentationData let semaphore: DispatchSemaphore? if synchronousLoad { semaphore = DispatchSemaphore(value: 0) } else { semaphore = nil } let syncResult = Atomic<(Bool, (() -> Void)?)>(value: (false, nil)) self.disposable.set(combineLatest(entriesSignal, self.animatedIn.get()).start(next: { [weak self] entries, animatedIn in let f: () -> Void = { if let strongSelf = self, animatedIn { let isFirstTime = strongSelf.entries.isEmpty var entries = entries if !isFirstTime, let updated = entries.first, case let .image(mediaId, imageReference, _, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, _, emojiMarkup) = updated, !videoRepresentations.isEmpty, let previous = strongSelf.entries.first, case let .topImage(representations, _, _, _, _, _) = previous { let firstEntry = AvatarGalleryEntry.image(mediaId, imageReference, representations, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, false, emojiMarkup) entries.remove(at: 0) entries.insert(firstEntry, at: 0) } strongSelf.entries = entries if strongSelf.centralEntryIndex == nil { strongSelf.centralEntryIndex = 0 } if strongSelf.isSuggested, let firstEntry = entries.first { strongSelf.navigationItem.title = !firstEntry.videoRepresentations.isEmpty ? strongSelf.presentationData.strings.Conversation_SuggestedVideoTitle : strongSelf.presentationData.strings.Conversation_SuggestedPhotoTitle } if strongSelf.isViewLoaded { strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: sourceCorners, delete: strongSelf.canDelete ? { self?.deleteEntry(entry) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) }, edit: { [weak self] in self?.editEntry(entry) }) }), centralItemIndex: strongSelf.centralEntryIndex, synchronous: !isFirstTime) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in strongSelf?.didSetReady = true } strongSelf._ready.set(ready |> map { true }) } } } var process = false let _ = syncResult.modify { processed, _ in if !processed { return (processed, f) } process = true return (true, nil) } semaphore?.signal() if process { Queue.mainQueue().async { f() } } })) if let semaphore = semaphore { let _ = semaphore.wait(timeout: DispatchTime.now() + 1.0) } var syncResultApply: (() -> Void)? let _ = syncResult.modify { processed, f in syncResultApply = f return (true, nil) } syncResultApply?() self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in if let strongSelf = self { strongSelf.navigationItem.setTitle(title, animated: strongSelf.navigationItem.title?.isEmpty ?? true) } })) self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in self?.navigationItem.titleView = titleView })) self.centralItemAttributesDisposable.add(self.centralItemRightBarButtonItems.get().start(next: { [weak self] rightBarButtonItems in self?.navigationItem.rightBarButtonItems = rightBarButtonItems })) self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) }, transition: .immediate) })) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.disposable.dispose() self.centralItemAttributesDisposable.dispose() self.editDisposable.dispose() } @objc func donePressed() { self.dismiss(forceAway: false) } private func dismissImmediately() { self._hiddenMedia.set(.single(nil)) self.presentingViewController?.dismiss(animated: false, completion: nil) } private func dismiss(forceAway: Bool) { self.animatedIn.set(false) var animatedOutNode = true var animatedOutInterface = false let completion = { [weak self] in if animatedOutNode && animatedOutInterface { self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) } } if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments { if !self.entries.isEmpty { var sourceHasRoundCorners = false if case .round = self.sourceCorners { sourceHasRoundCorners = true } if (centralItemNode.index == 0 || !sourceHasRoundCorners), let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { animatedOutNode = false centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { animatedOutNode = true completion() }) } } } self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { animatedOutInterface = true completion() }) } override public func loadDisplayNode() { let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in if let strongSelf = self { strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true) } }, pushController: { _ in }, dismissController: { [weak self] in self?.dismiss(forceAway: true) }, replaceRootController: { [weak self] controller, ready in if let strongSelf = self { strongSelf.replaceRootController(controller, ready) } }, editMedia: { _ in }, controller: { [weak self] in return self }) self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction) self.displayNodeDidLoad() self.galleryNode.pager.updateOnReplacement = true self.galleryNode.statusBar = self.statusBar self.galleryNode.navigationBar = self.navigationBar self.galleryNode.animateAlpha = false self.galleryNode.transitionDataForCentralItem = { [weak self] in if let strongSelf = self { if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? AvatarGalleryControllerPresentationArguments { var sourceHasRoundCorners = false if case .round = strongSelf.sourceCorners { sourceHasRoundCorners = true } if centralItemNode.index != 0 && sourceHasRoundCorners { return nil } if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface) } } } return nil } self.galleryNode.dismiss = { [weak self] in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) } let presentationData = self.presentationData self.galleryNode.pager.replaceItems(self.entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] in self?.deleteEntry(entry) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) }, edit: { [weak self] in self?.editEntry(entry) }) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { var hiddenItem: AvatarGalleryEntry? if let index = index { hiddenItem = strongSelf.entries[index] if let node = strongSelf.galleryNode.pager.centralItemNode() { strongSelf.centralItemTitle.set(node.title()) strongSelf.centralItemTitleView.set(node.titleView()) strongSelf.centralItemRightBarButtonItems.set(node.rightBarButtonItems()) strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } } if strongSelf.didSetReady { strongSelf._hiddenMedia.set(.single(hiddenItem)) } } } let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in self?.didSetReady = true } self._ready.set(ready |> map { true }) } override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) var nodeAnimatesItself = false if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments { self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) self.centralItemRightBarButtonItems.set(centralItemNode.rightBarButtonItems()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) { nodeAnimatesItself = true if presentationArguments.animated { self.animatedIn.set(false) centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { self.animatedIn.set(true) }) } self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) } } if !self.isPresentedInPreviewingContext() { self.galleryNode.setControlsHidden(false, animated: false) if let presentationArguments = self.presentationArguments as? AvatarGalleryControllerPresentationArguments { if presentationArguments.animated { self.galleryNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false) } } } } override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { return itemSize.aspectFitted(layout.size) } else { return nil } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) if !self.adjustedForInitialPreviewingLayout && self.isPresentedInPreviewingContext() { self.adjustedForInitialPreviewingLayout = true self.galleryNode.setControlsHidden(true, animated: false) if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() { self.preferredContentSize = itemSize.aspectFitted(self.view.bounds.size) self.containerLayoutUpdated(ContainerViewLayout(size: self.preferredContentSize, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) centralItemNode.activateAsInitial() } } } private var canDelete: Bool { let canDelete: Bool if self.peer.id == self.context.account.peerId { canDelete = true } else if case let .legacyGroup(group) = self.peer { switch group.role { case .creator, .admin: canDelete = true case .member: canDelete = false } } else if case let .channel(channel) = self.peer { canDelete = channel.hasPermission(.changeInfo) } else { canDelete = false } return canDelete } private func replaceEntries(_ entries: [AvatarGalleryEntry]) { self.galleryNode.currentThumbnailContainerNode?.updateSynchronously = true self.galleryNode.pager.replaceItems(entries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: self.peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] in self?.deleteEntry(entry) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) }, edit: { [weak self] in self?.editEntry(entry) }) }), centralItemIndex: 0, synchronous: true) self.entries = entries self.galleryNode.currentThumbnailContainerNode?.updateSynchronously = false } private func setMainEntry(_ rawEntry: AvatarGalleryEntry) { var entry = rawEntry if case .topImage = entry, !self.entries.isEmpty { entry = self.entries[0] } switch entry { case .topImage: if self.peer.id == self.context.account.peerId { } else { } case let .image(_, reference, _, _, _, _, _, _, _, _, _, _): if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer._asPeer()) { if let reference = reference { let _ = (self.context.engine.accountData.updatePeerPhotoExisting(reference: reference) |> deliverOnMainQueue).start(next: { [weak self] photo in if let strongSelf = self, let photo = photo, let firstEntry = strongSelf.entries.first, case let .image(_, _, _, _, _, index, indexData, messageId, _, caption, _, emojiMarkup) = firstEntry { let updatedEntry = AvatarGalleryEntry.image(photo.imageId, photo.reference, photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), strongSelf.peer, index, indexData, messageId, photo.immediateThumbnailData, caption, false, emojiMarkup) for (lhs, rhs) in zip(firstEntry.representations, updatedEntry.representations) { if lhs.representation.dimensions == rhs.representation.dimensions { strongSelf.context.account.postbox.mediaBox.copyResourceData(from: lhs.representation.resource.id, to: rhs.representation.resource.id, synchronous: true) } } for (lhs, rhs) in zip(firstEntry.videoRepresentations, updatedEntry.videoRepresentations) { if lhs.representation.dimensions == rhs.representation.dimensions { strongSelf.context.account.postbox.mediaBox.copyResourceData(from: lhs.representation.resource.id, to: rhs.representation.resource.id, synchronous: true) } } var entries = strongSelf.entries entries.remove(at: 0) entries.insert(updatedEntry, at: 0) strongSelf.replaceEntries(normalizeEntries(entries)) if let firstEntry = strongSelf.entries.first { strongSelf._hiddenMedia.set(.single(firstEntry)) } } }) } if let index = self.entries.firstIndex(of: entry) { var entries = self.entries let previousFirstEntry = entries.first entries.remove(at: index) entries.remove(at: 0) entries.insert(entry, at: 0) if let previousFirstEntry = previousFirstEntry { entries.insert(previousFirstEntry, at: index) } self.replaceEntries(normalizeEntries(entries)) if let firstEntry = self.entries.first { self._hiddenMedia.set(.single(firstEntry)) } } } } } private func editEntry(_ rawEntry: AvatarGalleryEntry) { let actionSheet = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak actionSheet] in actionSheet?.dismissAnimated() } var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: self.presentationData.strings.Settings_SetNewProfilePhotoOrVideo, color: .accent, action: { [weak self] in dismissAction() self?.openAvatarSetup?({ [weak self] in self?.dismissImmediately() }) })) var isFallback = false if case let .image(_, _, _, _, _, _, _, _, _, _, isFallbackValue, _) = rawEntry { isFallback = isFallbackValue } if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 || isFallback { let title: String if let _ = rawEntry.videoRepresentations.last { title = self.presentationData.strings.ProfilePhoto_SetMainVideo } else { title = self.presentationData.strings.ProfilePhoto_SetMainPhoto } items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak self] in dismissAction() self?.setMainEntry(rawEntry) })) } let deleteTitle: String if let _ = rawEntry.videoRepresentations.last { deleteTitle = self.presentationData.strings.Settings_RemoveVideo } else { deleteTitle = self.presentationData.strings.GroupInfo_SetGroupPhotoDelete } items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak self] in dismissAction() self?.deleteEntry(rawEntry) })) actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.view.endEditing(true) self.present(actionSheet, in: .window(.root)) } private func deleteEntry(_ rawEntry: AvatarGalleryEntry) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let proceed = { var entry = rawEntry if case .topImage = entry, !self.entries.isEmpty { entry = self.entries[0] } self.removedEntry?(rawEntry) var focusOnItem: Int? var updatedEntries = self.entries var replaceItems = false var dismiss = false switch entry { case .topImage: if self.peer.id == self.context.account.peerId { } else { if entry == self.entries.first { let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() dismiss = true } else { if let index = self.entries.firstIndex(of: entry) { self.entries.remove(at: index) self.galleryNode.pager.transaction(GalleryPagerTransaction(deleteItems: [index], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false)) } } } case let .image(_, reference, _, _, _, _, _, messageId, _, _, isFallback, _): if self.peer.id == self.context.account.peerId { if isFallback { let _ = self.context.engine.accountData.updateFallbackPhoto(resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() } else if let reference = reference { let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start() } if entry == self.entries.first { dismiss = true } else { if let index = self.entries.firstIndex(of: entry) { replaceItems = true updatedEntries.remove(at: index) focusOnItem = index - 1 } } } else { if let messageId = messageId { let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: [messageId], type: .forEveryone).start() } if entry == self.entries.first { let _ = self.context.engine.peers.updatePeerPhoto(peerId: self.peer.id, photo: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() dismiss = true } else { if let index = self.entries.firstIndex(of: entry) { replaceItems = true updatedEntries.remove(at: index) focusOnItem = index - 1 } } } } if replaceItems { updatedEntries = normalizeEntries(updatedEntries) self.galleryNode.pager.replaceItems(updatedEntries.map({ entry in PeerAvatarImageGalleryItem(context: self.context, peer: self.peer, presentationData: presentationData, entry: entry, sourceCorners: self.sourceCorners, delete: self.canDelete ? { [weak self] in self?.deleteEntry(entry) } : nil, setMain: { [weak self] in self?.setMainEntry(entry) }, edit: { [weak self] in self?.editEntry(entry) }) }), centralItemIndex: focusOnItem, synchronous: true) self.entries = updatedEntries } if dismiss { self._hiddenMedia.set(.single(nil)) Queue.mainQueue().after(0.2) { self.dismiss(forceAway: true) } } else { if let firstEntry = self.entries.first { self._hiddenMedia.set(.single(firstEntry)) } } } let actionSheet = ActionSheetController(presentationData: presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() proceed() }) ] actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) self.present(actionSheet, in: .window(.root)) } }