diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7bd70345dfb..ab9c48b2648 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -633,8 +633,8 @@ "bzlTransitiveDigest": "8/YWyYftd8THfVoADvrOmQLl45wUGfP2MVjLM5FFn50=", "usagesDigest": "voXBMcSNlo2fnK6JIvInIrncYhBKKG8nBeKvToaUA0Y=", "recordedFileInputs": { - "@@//Package.resolved": "8129edf94593b3f84e222b0e75b1ddf0c256af3606b0e9f51e13b3ebe7e344c9", - "@@//Package.swift": "0c4c991fedb7a7b66589da25fd19557ad938bb874d019e43e5aaa6d7e0a333d6" + "@@//Package.resolved": "7b3ab6b65f3eb4d3dc81d74f9d340fcd03ce07e24a38150376a825bf8aaaa4de", + "@@//Package.swift": "91f669b89d38025eea7f81c896dca68cd9faf81f2aba285cc7430b2a5514b38c" }, "recordedDirentsInputs": {}, "envVariables": {}, @@ -1138,7 +1138,7 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_assistant_ios", - "commit": "928c810bc05355a39b1fd18cdffd5abaaffc5ace", + "commit": "9a03d3c4b97508fc4aad0ec3b155af2c5379bbb2", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "version": "", "init_submodules": false, @@ -1404,7 +1404,7 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_wallet_ios", - "commit": "63ea2a8342b31467d251a406cf4e72f02fc842eb", + "commit": "466e4a1b29809f301818b5670345089e91a5adaa", "remote": "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "version": "", "init_submodules": false, diff --git a/Package.resolved b/Package.resolved index 3e4fde0ce03..ae8a4e9ebc9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -114,7 +114,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "fix/spy_on_friends", - "revision" : "928c810bc05355a39b1fd18cdffd5abaaffc5ace" + "revision" : "9a03d3c4b97508fc4aad0ec3b155af2c5379bbb2" } }, { @@ -123,7 +123,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "state" : { "branch" : "develop", - "revision" : "63ea2a8342b31467d251a406cf4e72f02fc842eb" + "revision" : "466e4a1b29809f301818b5670345089e91a5adaa" } }, { diff --git a/Package.swift b/Package.swift index a92b224253f..13e38cba823 100644 --- a/Package.swift +++ b/Package.swift @@ -6,6 +6,6 @@ let package = Package( name: "nicegram-package", dependencies: [ .package(url: "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", branch: "fix/spy_on_friends"), - .package(url: "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", branch: "develop") + .package(url: "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", branch: "develop") ] ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsEmptyDataNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsEmptyDataNode.swift new file mode 100644 index 00000000000..668fb06c508 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsEmptyDataNode.swift @@ -0,0 +1,118 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import ItemListUI +import FeatSpyOnFriends + +@available(iOS 15.0, *) +public final class SpyOnFriendsEmptyDataItem: ListViewItem, ItemListItem { + public let sectionId: ItemListSectionId + public let theme: PresentationTheme + + public init( + sectionId: ItemListSectionId, + theme: PresentationTheme + ) { + self.sectionId = sectionId + self.theme = theme + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { () -> Void in + let node = SpyOnFriendsEmptyDataNode() + node.setupItem(self) + + let (layout, apply) = node.asyncLayout()(self, params, false, false, false) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + if Thread.isMainThread { + configure() + } else { + Queue.mainQueue().async(configure) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? SpyOnFriendsEmptyDataNode { + let nodeLayout = nodeValue.asyncLayout() + + let (layout, apply) = nodeLayout(self, params, false, false, false) + + completion(layout, { _ in + apply(animation) + }) + } else { + assertionFailure() + } + } + } +} + +@available(iOS 15.0, *) +class SpyOnFriendsEmptyDataNode: ListViewItemNode { + var item: SpyOnFriendsEmptyDataItem? + + private let emptyDataView: SpyOnFriendsEmptyDataView + private let emptyDataNode: ASDisplayNode + + required init() { + let emptyDataView = SpyOnFriendsEmptyDataView() + self.emptyDataView = emptyDataView + self.emptyDataNode = ASDisplayNode { + emptyDataView + } + + super.init(layerBacked: false, dynamicBounce: false, rotated: false) + + self.addSubnode(emptyDataNode) + } + + func setupItem(_ item: SpyOnFriendsEmptyDataItem) { + self.item = item + } + + func asyncLayout() -> (_ item: SpyOnFriendsEmptyDataItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in + guard let self else { + return ( + ListViewItemNodeLayout( + contentSize: .zero, + insets: .zero + ), + { _ in } + ) + } + + emptyDataView.updateConstraintsIfNeeded() + + let emptyDataInsets: UIEdgeInsets = isPortrait ? .vertical(20).horizontal(16) : .vertical(20).horizontal(59) + + let size = CGSize( + width: params.width - (emptyDataInsets.left + emptyDataInsets.right), + height: 300 + ) + + let layout = ListViewItemNodeLayout( + contentSize: size, + insets: emptyDataInsets + ) + + let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in + guard let self else { return } + emptyDataNode.frame = CGRect(origin: .init(x: emptyDataInsets.left, y: 0), size: size) + } + + return (layout, apply) + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsHeaderNode.swift new file mode 100644 index 00000000000..3481ddbce87 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsHeaderNode.swift @@ -0,0 +1,151 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import ItemListUI +import FeatSpyOnFriends + +@available(iOS 15.0, *) +public final class SpyOnFriendsHeaderItem: ListViewItem, ItemListItem { + public let sectionId: ItemListSectionId + public let context: SpyOnFriendsContext + public let theme: PresentationTheme + public let locale: Locale + public let peerId: Int64 + public let isRefreshing: Bool + + public init( + sectionId: ItemListSectionId, + context: SpyOnFriendsContext, + theme: PresentationTheme, + locale: Locale, + peerId: Int64, + isRefreshing: Bool + ) { + self.sectionId = sectionId + self.context = context + self.theme = theme + self.locale = locale + self.peerId = peerId + self.isRefreshing = isRefreshing + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { () -> Void in + let node = SpyOnFriendsHeaderNode(peerId: self.peerId) + node.setupItem(self) + + let (layout, apply) = node.asyncLayout()(self, params, false, false, false) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + if Thread.isMainThread { + configure() + } else { + Queue.mainQueue().async(configure) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? SpyOnFriendsHeaderNode { + nodeValue.setupItem(self) + + let nodeLayout = nodeValue.asyncLayout() + + let (layout, apply) = nodeLayout(self, params, false, false, false) + + completion(layout, { _ in + apply(animation) + }) + } else { + assertionFailure() + } + } + } +} + +@available(iOS 15.0, *) +class SpyOnFriendsHeaderNode: ListViewItemNode { + var item: SpyOnFriendsHeaderItem? + + private let headerView: SpyOnFriendsHeaderView + private let headerNode: ASDisplayNode + + required init(peerId: Int64) { + let headerView = SpyOnFriendsHeaderView(peerId: peerId) + self.headerView = headerView + self.headerNode = ASDisplayNode { + headerView + } + + super.init(layerBacked: false, dynamicBounce: false, rotated: false) + + self.addSubnode(headerNode) + } + + func setupItem(_ item: SpyOnFriendsHeaderItem) { + self.item = item + + headerView.setup( + with: item.theme.list.itemAccentColor, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + locale: item.locale, + isRefreshing: item.isRefreshing + ) { + item.context.load() + } + } + + func asyncLayout() -> (_ item: SpyOnFriendsHeaderItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in + guard let self else { + return ( + ListViewItemNodeLayout( + contentSize: .zero, + insets: .zero + ), + { _ in } + ) + } + + headerView.setup( + with: item.theme.list.itemAccentColor, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + locale: item.locale, + isRefreshing: item.isRefreshing + ) { + item.context.load() + } + headerView.updateConstraintsIfNeeded() + + let headerInsets: UIEdgeInsets = isPortrait ? .vertical(12).horizontal(16) : .vertical(12).horizontal(59) + let headerSize = headerView.systemLayoutSizeFitting( + UIView.layoutFittingExpandedSize + ) + let size = CGSize( + width: params.width - (headerInsets.left + headerInsets.right), + height: headerSize.height + ) + + let layout = ListViewItemNodeLayout( + contentSize: size, + insets: headerInsets + ) + + let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in + guard let self else { return } + headerNode.frame = CGRect(origin: .init(x: headerInsets.left, y: 0), size: size) + } + + return (layout, apply) + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsMessagesNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsMessagesNode.swift new file mode 100644 index 00000000000..114940b4725 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsMessagesNode.swift @@ -0,0 +1,178 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import ItemListUI +import FeatSpyOnFriends +import AccountContext +import AvatarNode + +@available(iOS 15.0, *) +public final class SpyOnFriendsMessagesItem: ListViewItem, ItemListItem { + public let sectionId: ItemListSectionId + public let context: AccountContext + public let theme: PresentationTheme + public let locale: Locale + public let group: (Date, [SpyOnFriendsGroup]) + public let openMessage: (Int32) -> Void + + public init( + sectionId: ItemListSectionId, + context: AccountContext, + theme: PresentationTheme, + locale: Locale, + group: (Date, [SpyOnFriendsGroup]), + openMessage: @escaping (Int32) -> Void + ) { + self.sectionId = sectionId + self.context = context + self.theme = theme + self.locale = locale + self.group = group + self.openMessage = openMessage + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { () -> Void in + let node = SpyOnFriendsMessagesNode() + node.setupItem(self) + + let (layout, apply) = node.asyncLayout()(self, params, false, false, false) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + if Thread.isMainThread { + configure() + } else { + Queue.mainQueue().async(configure) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? SpyOnFriendsMessagesNode { + nodeValue.setupItem(self) + + let nodeLayout = nodeValue.asyncLayout() + + let (layout, apply) = nodeLayout(self, params, false, false, false) + + completion(layout, { _ in + apply(animation) + }) + } else { + assertionFailure() + } + } + } +} + +@available(iOS 15.0, *) +class SpyOnFriendsMessagesNode: ListViewItemNode { + var item: SpyOnFriendsMessagesItem? + + private let messagesView: SpyOnFriendsMessagesView + private let messagesNode: ASDisplayNode + + required init() { + let messagesView = SpyOnFriendsMessagesView() + self.messagesView = messagesView + self.messagesNode = ASDisplayNode { + messagesView + } + + super.init(layerBacked: false, dynamicBounce: false, rotated: false) + + self.addSubnode(messagesNode) + } + + func setupItem(_ item: SpyOnFriendsMessagesItem) { + self.item = item + + messagesView.setup( + with: item.group, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + locale: item.locale, + tapOnMessage: { id in + item.openMessage(id) + }, + logoLoader: { [weak self] peerId in + guard let self else { return nil } + + return try await self.peerAvatar(with: item.context, peerId: PeerId(peerId)).awaitForFirstValue() + } + ) + } + + func asyncLayout() -> (_ item: SpyOnFriendsMessagesItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in + guard let self else { + return ( + ListViewItemNodeLayout( + contentSize: .zero, + insets: .zero + ), + { _ in } + ) + } + + messagesView.setup( + with: item.group, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + locale: item.locale, + tapOnMessage: { id in + item.openMessage(id) + }, + logoLoader: { [weak self] peerId in + guard let self else { return nil } + + return try await self.peerAvatar(with: item.context, peerId: PeerId(peerId)).awaitForFirstValue() + } + ) + messagesView.updateConstraintsIfNeeded() + + let messagesInsets: UIEdgeInsets = isPortrait ? .bottom(32).horizontal(16) : .bottom(32).horizontal(59) + let messagesSize = messagesView.systemLayoutSizeFitting( + UIView.layoutFittingExpandedSize + ) + let size = CGSize( + width: params.width - (messagesInsets.left + messagesInsets.right), + height: messagesSize.height + ) + + let layout = ListViewItemNodeLayout( + contentSize: size, + insets: messagesInsets + ) + + let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in + guard let self else { return } + messagesNode.frame = CGRect(origin: .init(x: messagesInsets.left, y: 0), size: size) + } + + return (layout, apply) + } + } + + private func peerAvatar(with context: AccountContext, peerId: PeerId) -> Signal { + return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + guard let peer else { return .single(nil) } + + return peerAvatarCompleteImage( + account: context.account, + peer: peer, + forceProvidedRepresentation: false, + representation: nil, + size: CGSize(width: 50, height: 50) + ) + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsPaneNode.swift index a03abe1534a..5fd35a1e3af 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsPaneNode.swift @@ -5,19 +5,13 @@ import SwiftSignalKit import Postbox import TelegramPresentationData import AccountContext -import ContextUI -import PhotoResources -import TelegramUIPreferences -import ItemListPeerItem import MergeLists import ItemListUI import ChatControllerInteraction -import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode import FeatSpyOnFriends -import AvatarNode import TelegramApi -import FeatSpyOnFriends +import TelegramStringFormatting import NicegramWallet import NGUtils import NGData @@ -30,26 +24,29 @@ private struct SpyOnFriendsListTransaction { @available(iOS 15.0, *) private enum SpyOnFriendsListEntry: Comparable, Identifiable { - case header(sectionId: ItemListSectionId, context: SpyOnFriendsContext) + case header(sectionId: ItemListSectionId, context: SpyOnFriendsContext, isRefreshing: Bool) case group(sectionId: ItemListSectionId, context: AccountContext, group: (Date, [SpyOnFriendsGroup])) case unlock(sectionId: ItemListSectionId, context: SpyOnFriendsContext) + case emptyData(sectionId: ItemListSectionId) var stableId: Int32 { switch self { - case let .header(sectionId, _): + case let .header(sectionId, _, _): return sectionId case let .group(sectionId, _, _): return sectionId case let .unlock(sectionId, _): return sectionId + case let .emptyData(sectionId): + return sectionId } } static func ==(lhs: SpyOnFriendsListEntry, rhs: SpyOnFriendsListEntry) -> Bool { switch lhs { - case let .header(lhsSectionId, _): - if case let .header(rhsSectionId, _) = rhs { - return lhsSectionId == rhsSectionId + case let .header(_, _, lhsIsRefreshing): + if case let .header(_, _, rhsIsRefreshing) = rhs { + return lhsIsRefreshing == rhsIsRefreshing } else { return false } @@ -65,6 +62,8 @@ private enum SpyOnFriendsListEntry: Comparable, Identifiable { } else { return false } + case .emptyData: + return true } } @@ -74,10 +73,10 @@ private enum SpyOnFriendsListEntry: Comparable, Identifiable { switch rhs { case let .group(_, _, rhsGroups): return lhsGroups.0 < rhsGroups.0 - case .header, .unlock: + case .header, .unlock, .emptyData: return false } - case .header, .unlock: + case .header, .unlock, .emptyData: return false } } @@ -90,18 +89,21 @@ private enum SpyOnFriendsListEntry: Comparable, Identifiable { share: @escaping () -> Void ) -> ListViewItem { switch self { - case let .header(sectionId, context): + case let .header(sectionId, context, isRefreshing): return SpyOnFriendsHeaderItem( sectionId: sectionId, context: context, theme: presentationData.theme, - peerId: peerId.id._internalGetInt64Value() + locale: localeWithStrings(presentationData.strings), + peerId: peerId.id._internalGetInt64Value(), + isRefreshing: isRefreshing ) case let .group(sectionId, context, group): return SpyOnFriendsMessagesItem( sectionId: sectionId, context: context, theme: presentationData.theme, + locale: localeWithStrings(presentationData.strings), group: group, openMessage: openMessage ) @@ -109,11 +111,17 @@ private enum SpyOnFriendsListEntry: Comparable, Identifiable { return SpyOnFriendsUnlockItem( sectionId: sectionId, theme: presentationData.theme, + locale: localeWithStrings(presentationData.strings), context: context, accountContext: accountContext, peerId: peerId, share: share ) + case let .emptyData(sectionId): + return SpyOnFriendsEmptyDataItem( + sectionId: sectionId, + theme: presentationData.theme + ) } } } @@ -357,21 +365,30 @@ final class SpyOnFriendsPaneNode: ASDisplayNode, PeerInfoPaneNode { var entries: [SpyOnFriendsListEntry] = [] if isPremium() || isPremiumPlus() { - entries.append(.header(sectionId: 0, context: spyOnFriendsContext)) + entries.append(.header( + sectionId: 0, + context: spyOnFriendsContext, + isRefreshing: state.dataState != .ready(canLoadMore: true) + )) let groups = groups(from: state.chatsWithMessages) - let groupsEntries = groups.map { - SpyOnFriendsListEntry.group( - sectionId: ItemListSectionId($0.0.timeIntervalSince1970), - context: context, - group: $0 - ) + if groups.isEmpty && + state.dataState == .ready(canLoadMore: true) { + entries.append(.emptyData(sectionId: 3)) + } else { + let groupsEntries = groups.map { + SpyOnFriendsListEntry.group( + sectionId: ItemListSectionId($0.0.timeIntervalSince1970), + context: context, + group: $0 + ) + } + + entries.append(contentsOf: groupsEntries) } - - entries.append(contentsOf: groupsEntries) } else { - entries.append(.unlock(sectionId: 1, context: spyOnFriendsContext)) + entries = [.unlock(sectionId: 1, context: spyOnFriendsContext)] } let transaction = preparedTransition( @@ -447,9 +464,7 @@ final class SpyOnFriendsPaneNode: ASDisplayNode, PeerInfoPaneNode { private func share() { let controller = context.sharedContext.makeContactSelectionController(.init( context: context, - title: { strings in - "" - } + title: { _ in "" } )) controller.navigationPresentation = .modal @@ -524,454 +539,6 @@ final class SpyOnFriendsPaneNode: ASDisplayNode, PeerInfoPaneNode { } } -@available(iOS 15.0, *) -public final class SpyOnFriendsHeaderItem: ListViewItem, ItemListItem { - public let sectionId: ItemListSectionId - public let context: SpyOnFriendsContext - public let theme: PresentationTheme - public let peerId: Int64 - - public init( - sectionId: ItemListSectionId, - context: SpyOnFriendsContext, - theme: PresentationTheme, - peerId: Int64 - ) { - self.sectionId = sectionId - self.context = context - self.theme = theme - self.peerId = peerId - } - - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - let configure = { () -> Void in - let node = SpyOnFriendsHeaderNode(peerId: self.peerId) - node.setupItem(self) - - let (layout, apply) = node.asyncLayout()(self, params, false, false, false) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - completion(node, { - return (nil, { _ in apply(.None) }) - }) - } - if Thread.isMainThread { - configure() - } else { - Queue.mainQueue().async(configure) - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? SpyOnFriendsHeaderNode { - nodeValue.setupItem(self) - - let nodeLayout = nodeValue.asyncLayout() - - let (layout, apply) = nodeLayout(self, params, false, false, false) - - completion(layout, { _ in - apply(animation) - }) - } else { - assertionFailure() - } - } - } -} - -@available(iOS 15.0, *) -class SpyOnFriendsHeaderNode: ListViewItemNode { - var item: SpyOnFriendsHeaderItem? - - private let headerView: SpyOnFriendsHeaderView - private let headerNode: ASDisplayNode - - required init(peerId: Int64) { - let headerView = SpyOnFriendsHeaderView(peerId: peerId) - self.headerView = headerView - self.headerNode = ASDisplayNode { - headerView - } - - super.init(layerBacked: false, dynamicBounce: false, rotated: false) - - self.addSubnode(headerNode) - } - - func setupItem(_ item: SpyOnFriendsHeaderItem) { - self.item = item - - headerView.setup( - with: item.theme.list.itemAccentColor, - backgroundColor: item.theme.list.itemBlocksBackgroundColor - ) { - item.context.load() - } - } - - func asyncLayout() -> (_ item: SpyOnFriendsHeaderItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in - guard let self else { - return ( - ListViewItemNodeLayout( - contentSize: .zero, - insets: .zero - ), - { _ in } - ) - } - - headerView.setup( - with: item.theme.list.itemAccentColor, - backgroundColor: item.theme.list.itemBlocksBackgroundColor - ) { - item.context.load() - } - headerView.updateConstraintsIfNeeded() - - let headerInsets: UIEdgeInsets = isPortrait ? .vertical(12).horizontal(16) : .vertical(12).horizontal(59) - let headerSize = headerView.systemLayoutSizeFitting( - UIView.layoutFittingExpandedSize - ) - let size = CGSize( - width: params.width - (headerInsets.left + headerInsets.right), - height: headerSize.height - ) - - let layout = ListViewItemNodeLayout( - contentSize: size, - insets: headerInsets - ) - - let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in - guard let self else { return } - headerNode.frame = CGRect(origin: .init(x: headerInsets.left, y: 0), size: size) - } - - return (layout, apply) - } - } -} - -extension ListViewItemNode { - var isPortrait: Bool { - (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation.isPortrait ?? false - } -} - -@available(iOS 15.0, *) -public final class SpyOnFriendsMessagesItem: ListViewItem, ItemListItem { - public let sectionId: ItemListSectionId - public let context: AccountContext - public let theme: PresentationTheme - public let group: (Date, [SpyOnFriendsGroup]) - public let openMessage: (Int32) -> Void - - public init( - sectionId: ItemListSectionId, - context: AccountContext, - theme: PresentationTheme, - group: (Date, [SpyOnFriendsGroup]), - openMessage: @escaping (Int32) -> Void - ) { - self.sectionId = sectionId - self.context = context - self.theme = theme - self.group = group - self.openMessage = openMessage - } - - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - let configure = { () -> Void in - let node = SpyOnFriendsMessagesNode() - node.setupItem(self) - - let (layout, apply) = node.asyncLayout()(self, params, false, false, false) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - completion(node, { - return (nil, { _ in apply(.None) }) - }) - } - if Thread.isMainThread { - configure() - } else { - Queue.mainQueue().async(configure) - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? SpyOnFriendsMessagesNode { - nodeValue.setupItem(self) - - let nodeLayout = nodeValue.asyncLayout() - - let (layout, apply) = nodeLayout(self, params, false, false, false) - - completion(layout, { _ in - apply(animation) - }) - } else { - assertionFailure() - } - } - } -} - -@available(iOS 15.0, *) -class SpyOnFriendsMessagesNode: ListViewItemNode { - var item: SpyOnFriendsMessagesItem? - - private let messagesView: SpyOnFriendsMessagesView - private let messagesNode: ASDisplayNode - - required init() { - let messagesView = SpyOnFriendsMessagesView() - self.messagesView = messagesView - self.messagesNode = ASDisplayNode { - messagesView - } - - super.init(layerBacked: false, dynamicBounce: false, rotated: false) - - self.addSubnode(messagesNode) - } - - func setupItem(_ item: SpyOnFriendsMessagesItem) { - self.item = item - - messagesView.setup( - with: item.group, - backgroundColor: item.theme.list.itemBlocksBackgroundColor, - tapOnMessage: { id in - item.openMessage(id) - }, - logoLoader: { [weak self] peerId in - guard let self else { return nil } - - return try await self.peerAvatar(with: item.context, peerId: PeerId(peerId)).awaitForFirstValue() - } - ) - } - - func asyncLayout() -> (_ item: SpyOnFriendsMessagesItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in - guard let self else { - return ( - ListViewItemNodeLayout( - contentSize: .zero, - insets: .zero - ), - { _ in } - ) - } - - messagesView.setup( - with: item.group, - backgroundColor: item.theme.list.itemBlocksBackgroundColor, - tapOnMessage: { id in - item.openMessage(id) - }, - logoLoader: { [weak self] peerId in - guard let self else { return nil } - - return try await self.peerAvatar(with: item.context, peerId: PeerId(peerId)).awaitForFirstValue() - } - ) - messagesView.updateConstraintsIfNeeded() - - let messagesInsets: UIEdgeInsets = isPortrait ? .bottom(32).horizontal(16) : .bottom(32).horizontal(59) - let messagesSize = messagesView.systemLayoutSizeFitting( - UIView.layoutFittingExpandedSize - ) - let size = CGSize( - width: params.width - (messagesInsets.left + messagesInsets.right), - height: messagesSize.height - ) - - let layout = ListViewItemNodeLayout( - contentSize: size, - insets: messagesInsets - ) - - let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in - guard let self else { return } - messagesNode.frame = CGRect(origin: .init(x: messagesInsets.left, y: 0), size: size) - } - - return (layout, apply) - } - } - - private func peerAvatar(with context: AccountContext, peerId: PeerId) -> Signal { - return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> mapToSignal { peer -> Signal in - guard let peer else { return .single(nil) } - - return peerAvatarCompleteImage( - account: context.account, - peer: peer, - forceProvidedRepresentation: false, - representation: nil, - size: CGSize(width: 50, height: 50) - ) - } - } -} - -@available(iOS 15.0, *) -public final class SpyOnFriendsUnlockItem: ListViewItem, ItemListItem { - public let sectionId: ItemListSectionId - public let theme: PresentationTheme - public let context: SpyOnFriendsContext - public let accountContext: AccountContext - public let peerId: PeerId - public let share: () -> Void - - public init( - sectionId: ItemListSectionId, - theme: PresentationTheme, - context: SpyOnFriendsContext, - accountContext: AccountContext, - peerId: PeerId, - share: @escaping () -> Void - ) { - self.sectionId = sectionId - self.theme = theme - self.context = context - self.accountContext = accountContext - self.peerId = peerId - self.share = share - } - - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - let configure = { () -> Void in - let node = SpyOnFriendsUnlockNode(peerId: self.peerId.id._internalGetInt64Value()) - node.setupItem(self) - - let (layout, apply) = node.asyncLayout()(self, params, false, false, false) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - completion(node, { - return (nil, { _ in apply(.None) }) - }) - } - if Thread.isMainThread { - configure() - } else { - Queue.mainQueue().async(configure) - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? SpyOnFriendsUnlockNode { - let nodeLayout = nodeValue.asyncLayout() - - let (layout, apply) = nodeLayout(self, params, false, false, false) - - completion(layout, { _ in - apply(animation) - }) - } else { - assertionFailure() - } - } - } -} - -@available(iOS 15.0, *) -class SpyOnFriendsUnlockNode: ListViewItemNode { - var item: SpyOnFriendsUnlockItem? - - private let unlockView: SpyOnFriendsUnlockView - private let unlockNode: ASDisplayNode - - required init(peerId: Int64) { - let unlockView = SpyOnFriendsUnlockView(peerId: peerId) - self.unlockView = unlockView - self.unlockNode = ASDisplayNode { - unlockView - } - - super.init(layerBacked: false, dynamicBounce: false, rotated: false) - - self.addSubnode(unlockNode) - } - - func setupItem(_ item: SpyOnFriendsUnlockItem) { - self.item = item - - unlockView.setup( - with: item.theme.list.itemAccentColor, - backgroundColor: item.theme.list.itemBlocksBackgroundColor, - textColor: item.theme.list.blocksBackgroundColor - ) { - item.context.load() - } share: { - item.share() - } - unlockView.rotate() - } - - func asyncLayout() -> (_ item: SpyOnFriendsUnlockItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in - guard let self else { - return ( - ListViewItemNodeLayout( - contentSize: .zero, - insets: .zero - ), - { _ in } - ) - } - - unlockView.setup( - with: item.theme.list.itemAccentColor, - backgroundColor: item.theme.list.itemBlocksBackgroundColor, - textColor: item.theme.list.blocksBackgroundColor - ) { - item.context.load() - } share: { - item.share() - } - unlockView.rotate() - unlockView.updateConstraintsIfNeeded() - - let unlockInsets: UIEdgeInsets = isPortrait ? .top(20).bottom(64).horizontal(16) : .top(20).bottom(32).horizontal(59) - let unlockSize = unlockView.systemLayoutSizeFitting( - UIView.layoutFittingExpandedSize - ) - - let size = CGSize( - width: params.width - (unlockInsets.left + unlockInsets.right), - height: unlockSize.height + unlockInsets.bottom - ) - - let layout = ListViewItemNodeLayout( - contentSize: size, - insets: unlockInsets - ) - - let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in - guard let self else { return } - unlockNode.frame = CGRect(origin: .init(x: unlockInsets.left, y: 0), size: size) - } - - return (layout, apply) - } - } -} - private extension Api.Chat { var peerId: PeerId { switch self { @@ -989,6 +556,12 @@ private extension Api.Chat { } } +extension ListViewItemNode { + var isPortrait: Bool { + (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation.isPortrait ?? false + } +} + extension Int32 { var dateWithoutTime: Date { let date = Date(timeIntervalSince1970: TimeInterval(self)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsUnlockNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsUnlockNode.swift new file mode 100644 index 00000000000..088031b01c5 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/SpyOnFriends/SpyOnFriendsUnlockNode.swift @@ -0,0 +1,160 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import ItemListUI +import FeatSpyOnFriends +import AccountContext + +@available(iOS 15.0, *) +public final class SpyOnFriendsUnlockItem: ListViewItem, ItemListItem { + public let sectionId: ItemListSectionId + public let theme: PresentationTheme + public let locale: Locale + public let context: SpyOnFriendsContext + public let accountContext: AccountContext + public let peerId: PeerId + public let share: () -> Void + + public init( + sectionId: ItemListSectionId, + theme: PresentationTheme, + locale: Locale, + context: SpyOnFriendsContext, + accountContext: AccountContext, + peerId: PeerId, + share: @escaping () -> Void + ) { + self.sectionId = sectionId + self.theme = theme + self.locale = locale + self.context = context + self.accountContext = accountContext + self.peerId = peerId + self.share = share + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + let configure = { () -> Void in + let node = SpyOnFriendsUnlockNode(peerId: self.peerId.id._internalGetInt64Value()) + node.setupItem(self) + + let (layout, apply) = node.asyncLayout()(self, params, false, false, false) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { _ in apply(.None) }) + }) + } + if Thread.isMainThread { + configure() + } else { + Queue.mainQueue().async(configure) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? SpyOnFriendsUnlockNode { + let nodeLayout = nodeValue.asyncLayout() + + let (layout, apply) = nodeLayout(self, params, false, false, false) + + completion(layout, { _ in + apply(animation) + }) + } else { + assertionFailure() + } + } + } +} + +@available(iOS 15.0, *) +class SpyOnFriendsUnlockNode: ListViewItemNode { + var item: SpyOnFriendsUnlockItem? + + private let unlockView: SpyOnFriendsUnlockView + private let unlockNode: ASDisplayNode + + required init(peerId: Int64) { + let unlockView = SpyOnFriendsUnlockView(peerId: peerId) + self.unlockView = unlockView + self.unlockNode = ASDisplayNode { + unlockView + } + + super.init(layerBacked: false, dynamicBounce: false, rotated: false) + + self.addSubnode(unlockNode) + } + + func setupItem(_ item: SpyOnFriendsUnlockItem) { + self.item = item + + unlockView.setup( + with: item.theme.list.itemAccentColor, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + textColor: item.theme.list.blocksBackgroundColor, + locale: item.locale + ) { + item.context.load() + } share: { + item.share() + } + unlockView.rotate() + } + + func asyncLayout() -> (_ item: SpyOnFriendsUnlockItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) in + guard let self else { + return ( + ListViewItemNodeLayout( + contentSize: .zero, + insets: .zero + ), + { _ in } + ) + } + + unlockView.setup( + with: item.theme.list.itemAccentColor, + backgroundColor: item.theme.list.itemBlocksBackgroundColor, + textColor: item.theme.list.blocksBackgroundColor, + locale: item.locale + ) { + item.context.load() + } share: { + item.share() + } + unlockView.rotate() + unlockView.updateConstraintsIfNeeded() + + let unlockInsets: UIEdgeInsets = isPortrait ? .top(20).bottom(64).horizontal(16) : .top(20).bottom(32).horizontal(59) + let unlockSize = unlockView.systemLayoutSizeFitting( + UIView.layoutFittingExpandedSize + ) + + let size = CGSize( + width: params.width - (unlockInsets.left + unlockInsets.right), + height: unlockSize.height + unlockInsets.bottom + ) + + let layout = ListViewItemNodeLayout( + contentSize: size, + insets: unlockInsets + ) + + let apply: (ListViewItemUpdateAnimation) -> Void = { [weak self] _ in + guard let self else { return } + unlockNode.frame = CGRect(origin: .init(x: unlockInsets.left, y: 0), size: size) + } + + return (layout, apply) + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index ed75e0638da..b8bd3cc10c5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1031,7 +1031,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen // MARK: Nicegram NCG-7303 Spy on friends let feature = SpyOnFriendsFeature(navigator: SpyOnFriendsNavigatorImpl()) - if isPremium() || isPremiumPlus() { + if (isPremium() || isPremiumPlus()) && + feature.checkIfNeedUpdate(with: userPeerId.id._internalGetInt64Value()) { feature.updateLastUpdated(with: userPeerId.id._internalGetInt64Value()) sendSpyOnFriendsAnalytics(with: .usage) }