diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7663325f82..92f2b48d50 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6923,3 +6923,8 @@ Sorry for the inconvenience."; "InviteLink.InviteLinkForwardTooltip.TwoChats.One" = "Invite link forwarded to **%@** and **%@**"; "InviteLink.InviteLinkForwardTooltip.ManyChats.One" = "Invite link forwarded to **%@** and %@ others"; "InviteLink.InviteLinkForwardTooltip.SavedMessages.One" = "Invite link forwarded to **Saved Messages**"; + +"Conversation.RequestToJoinChannel" = "REQUEST TO JOIN"; +"Conversation.RequestToJoinGroup" = "REQUEST TO JOIN"; + +"Channel.AdminLog.JoinedViaRequest" = "%1$@ joined via invite link %2$@, approved by %3$@"; diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index 85fa60baae..528c7f5c20 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -55,6 +55,7 @@ swift_library( "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/AvatarNode:AvatarNode", "//submodules/LocalizedPeerData:LocalizedPeerData", + "//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 6eced106c6..6b17f12d5b 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -52,6 +52,8 @@ private enum InviteLinkViewEntryId: Hashable { case link case creatorHeader case creator + case requestHeader + case request(EnginePeer.Id) case importerHeader case importer(EnginePeer.Id) } @@ -60,6 +62,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { case link(PresentationTheme, ExportedInvitation) case creatorHeader(PresentationTheme, String) case creator(PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) + case requestHeader(PresentationTheme, String, String, Bool) + case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case importerHeader(PresentationTheme, String, String, Bool) case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) @@ -71,6 +75,10 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { return .creatorHeader case .creator: return .creator + case .requestHeader: + return .requestHeader + case let .request(_, _, _, peer, _, _): + return .request(peer.id) case .importerHeader: return .importerHeader case let .importer(_, _, _, peer, _, _): @@ -98,6 +106,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } + case let .requestHeader(lhsTheme, lhsTitle, lhsSubtitle, lhsExpired): + if case let .requestHeader(rhsTheme, rhsTitle, rhsSubtitle, rhsExpired) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsExpired == rhsExpired { + return true + } else { + return false + } + case let .request(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading): + if case let .request(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsLoading == rhsLoading { + return true + } else { + return false + } case let .importerHeader(lhsTheme, lhsTitle, lhsSubtitle, lhsExpired): if case let .importerHeader(rhsTheme, rhsTitle, rhsSubtitle, rhsExpired) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsExpired == rhsExpired { return true @@ -119,33 +139,49 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { switch rhs { case .link: return false - case .creatorHeader, .creator, .importerHeader, .importer: + case .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creatorHeader: switch rhs { case .link, .creatorHeader: return false - case .creator, .importerHeader, .importer: + case .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creator: switch rhs { case .link, .creatorHeader, .creator: return false - case .importerHeader, .importer: + case .requestHeader, .request, .importerHeader, .importer: return true } + case .requestHeader: + switch rhs { + case .link, .creatorHeader, .creator, .requestHeader: + return false + case .request, .importerHeader, .importer: + return true + } + case let .request(lhsIndex, _, _, _, _, _): + switch rhs { + case .link, .creatorHeader, .creator, .requestHeader: + return false + case let .request(rhsIndex, _, _, _, _, _): + return lhsIndex < rhsIndex + case .importerHeader, .importer: + return true + } case .importerHeader: switch rhs { - case .link, .creatorHeader, .importerHeader: + case .link, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: return false - case .creator, .importer: + case .importer: return true } case let .importer(lhsIndex, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .importerHeader: + case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: return false case let .importer(rhsIndex, _, _, _, _, _): return lhsIndex < rhsIndex @@ -175,7 +211,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) - case let .importerHeader(_, title, subtitle, expired): + case let .importerHeader(_, title, subtitle, expired), let .requestHeader(_, title, subtitle, expired): let additionalText: SectionHeaderAdditionalText if !subtitle.isEmpty { if expired { @@ -187,7 +223,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { additionalText = .none } return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText) - case let .importer(_, _, dateTimeFormat, peer, date, loading): + case let .importer(_, _, dateTimeFormat, peer, date, loading), let .request(_, _, dateTimeFormat, peer, date, loading): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) @@ -637,14 +673,47 @@ public final class InviteLinkViewController: ViewController { entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), invite.date)) if !requestsState.importers.isEmpty || (state.isLoadingMore && requestsState.count > 0) { - entries.append(.importerHeader(presentationData.theme, presentationData.strings.MemberRequests_PeopleRequested(Int32(requestsState.count)).uppercased(), "", false)) + entries.append(.requestHeader(presentationData.theme, presentationData.strings.MemberRequests_PeopleRequested(Int32(requestsState.count)).uppercased(), "", false)) } - let count: Int32 - let loading: Bool - + var count: Int32 + var loading: Bool var index: Int32 = 0 if requestsState.importers.isEmpty && requestsState.isLoadingMore { + count = min(4, state.count) + loading = true + let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + for i in 0 ..< count { + entries.append(.request(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) + } + } else { + count = min(4, Int32(requestsState.importers.count)) + loading = false + for importer in requestsState.importers { + if let peer = importer.peer.peer { + entries.append(.request(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, false)) + } + index += 1 + } + } + + if !state.importers.isEmpty || (state.isLoadingMore && state.count > 0) { + let subtitle: String + let subtitleExpired: Bool + if let usageLimit = invite.usageLimit { + let remaining = max(0, usageLimit - state.count) + subtitle = presentationData.strings.InviteLink_PeopleRemaining(remaining).uppercased() + subtitleExpired = remaining <= 0 + } else { + subtitle = "" + subtitleExpired = false + } + + entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased(), subtitle, subtitleExpired)) + } + + index = 0 + if state.importers.isEmpty && state.isLoadingMore { count = min(4, state.count) loading = true let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) @@ -652,9 +721,9 @@ public final class InviteLinkViewController: ViewController { entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) } } else { - count = min(4, Int32(requestsState.importers.count)) + count = min(4, Int32(state.importers.count)) loading = false - for importer in requestsState.importers { + for importer in state.importers { if let peer = importer.peer.peer { entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, false)) } @@ -662,43 +731,6 @@ public final class InviteLinkViewController: ViewController { } } - // if !state.importers.isEmpty || (state.isLoadingMore && state.count > 0) { - // let subtitle: String - // let subtitleExpired: Bool - // if let usageLimit = invite.usageLimit { - // let remaining = max(0, usageLimit - state.count) - // subtitle = presentationData.strings.InviteLink_PeopleRemaining(remaining).uppercased() - // subtitleExpired = remaining <= 0 - // } else { - // subtitle = "" - // subtitleExpired = false - // } - // - // entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased(), subtitle, subtitleExpired)) - // } - -// let count: Int32 -// let loading: Bool -// -// var index: Int32 = 0 -// if state.importers.isEmpty && state.isLoadingMore { -// count = min(4, state.count) -// loading = true -// let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) -// for i in 0 ..< count { -// entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) -// } -// } else { -// count = min(4, Int32(state.importers.count)) -// loading = false -// for importer in state.importers { -// if let peer = importer.peer.peer { -// entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, false)) -// } -// index += 1 -// } -// } - let previousCount = previousCount.swap(count) let previousLoading = previousLoading.swap(loading) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index f6827a8dbe..d5292038f7 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -151,21 +151,29 @@ private func inviteRequestsControllerEntries(presentationData: PresentationData, return entries } +private struct InviteRequestsControllerState: Equatable { + var searchingMembers: Bool +} + public func inviteRequestsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, existingContext: PeerInvitationImportersContext? = nil) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var navigateToProfileImpl: ((EnginePeer) -> Void)? - + var dismissInputImpl: (() -> Void)? var dismissTooltipsImpl: (() -> Void)? let actionsDisposable = DisposableSet() + let statePromise = ValuePromise(InviteRequestsControllerState(searchingMembers: false), ignoreRepeated: true) + let stateValue = Atomic(value: InviteRequestsControllerState(searchingMembers: false)) + let updateState: ((InviteRequestsControllerState) -> InviteRequestsControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + let updateDisposable = MetaDisposable() actionsDisposable.add(updateDisposable) - - var getControllerImpl: (() -> ViewController?)? - + let importersContext = existingContext ?? context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil)) let arguments = InviteRequestsControllerArguments(context: context, openLinks: { @@ -195,7 +203,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio }, denyRequest: { peer in importersContext.update(peer.id, action: .deny) }, peerContextAction: { peer, node, gesture in - guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?() else { + guard let node = node as? ContextExtractedContentContainingNode else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -206,11 +214,15 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio }, action: { _, f in f(.dismissWithoutContent) - dismissTooltipsImpl?() - }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let dismissPromise = ValuePromise(false) + let source = InviteRequestsContextExtractedContentSource(sourceNode: node, keepInPlace: false, blurBackground: true, centerVertically: true, shouldBeDismissed: dismissPromise.get()) +// sourceNode.requestDismiss = { +// dismissPromise.set(true) +// } + + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(items: items)), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }) @@ -222,9 +234,10 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ), - importersContext.state + importersContext.state, + statePromise.get() ) - |> map { presentationData, peer, importersState -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, peer, importersState, state -> (ItemListControllerState, (ItemListNodeState, Any)) in var isGroup = true if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false @@ -241,9 +254,43 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio let crossfade = !previousEntries.isEmpty && entries.isEmpty let animateChanges = (!previousEntries.isEmpty && !entries.isEmpty) && previousEntries.count != entries.count + let rightNavigationButton: ItemListNavigationButton? + if !importersState.importers.isEmpty { + rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: { + updateState { state in + var updatedState = state + updatedState.searchingMembers = true + return updatedState + } + }) + } else { + rightNavigationButton = nil + } + + var searchItem: ItemListControllerSearch? + if state.searchingMembers && !importersState.importers.isEmpty { + searchItem = InviteRequestsSearchItem(context: context, peerId: peerId, cancel: { + updateState { state in + var updatedState = state + updatedState.searchingMembers = false + return updatedState + } + }, openPeer: { peer in + arguments.openPeer(peer) + }, approveRequest: { peer in + arguments.approveRequest(peer) + }, denyRequest: { peer in + arguments.denyRequest(peer) + }, pushController: { c in + pushControllerImpl?(c) + }, dismissInput: { + dismissInputImpl?() + }) + } + let title: ItemListControllerTitle = .text(presentationData.strings.MemberRequests_Title) - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfade, animateChanges: animateChanges) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -283,8 +330,8 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio navigationController.pushViewController(controller) } } - getControllerImpl = { [weak controller] in - return controller + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) } dismissTooltipsImpl = { [weak controller] in controller?.window?.forEachController({ controller in @@ -301,3 +348,31 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio } return controller } + + +private final class InviteRequestsContextExtractedContentSource: ContextExtractedContentSource { + var keepInPlace: Bool + let ignoreContentTouches: Bool = false + let blurBackground: Bool + + private let sourceNode: ContextExtractedContentContainingNode + + var centerVertically: Bool + var shouldBeDismissed: Signal + + init(sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal) { + self.sourceNode = sourceNode + self.keepInPlace = keepInPlace + self.blurBackground = blurBackground + self.centerVertically = centerVertically + self.shouldBeDismissed = shouldBeDismissed + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift b/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift index 6956a7bfc1..2147a606e9 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsEmptyItem.swift @@ -70,6 +70,8 @@ final class InviteRequestsEmptyStateItemNode: ItemListControllerEmptyStateItemNo super.init() + self.isUserInteractionEnabled = false + self.addSubnode(self.animationNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift b/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift new file mode 100644 index 0000000000..c2d3ce7487 --- /dev/null +++ b/submodules/InviteLinksUI/Sources/InviteRequestsSearchItem.swift @@ -0,0 +1,598 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import ItemListUI +import PresentationDataUtils +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import SearchBarNode +import MergeLists +import ChatListSearchItemHeader +import ItemListUI +import SearchUI + +private let searchBarFont = Font.regular(17.0) + +final class SearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode { + private var theme: PresentationTheme + private let strings: PresentationStrings + + private let cancel: () -> Void + + private let searchBar: SearchBarNode + + private var queryUpdated: ((String) -> Void)? + var activity: Bool = false { + didSet { + searchBar.activity = activity + } + } + init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) { + self.theme = theme + self.strings = strings + + self.cancel = cancel + + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.cancel() + } + + self.searchBar.textUpdated = { [weak self] query, _ in + self?.queryUpdated?(query) + } + + updateActivity({ [weak self] value in + self?.activity = value + }) + + self.updatePlaceholder() + } + + func setQueryUpdated(_ f: @escaping (String) -> Void) { + self.queryUpdated = f + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: self.theme), strings: self.strings) + self.updatePlaceholder() + } + + func updatePlaceholder() { + self.searchBar.placeholderString = NSAttributedString(string: self.strings.Conversation_SearchByName_Placeholder, font: searchBarFont, textColor: self.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + } + + override var nominalHeight: CGFloat { + return 56.0 + } + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0)) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} + + +final class InviteRequestsSearchItem: ItemListControllerSearch { + let context: AccountContext + let peerId: PeerId + let cancel: () -> Void + let openPeer: (EnginePeer) -> Void + let approveRequest: (EnginePeer) -> Void + let denyRequest: (EnginePeer) -> Void + let pushController: (ViewController) -> Void + let dismissInput: () -> Void + + private var updateActivity: ((Bool) -> Void)? + private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) + private let activityDisposable = MetaDisposable() + + init(context: AccountContext, peerId: PeerId, cancel: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, approveRequest: @escaping (EnginePeer) -> Void, denyRequest: @escaping (EnginePeer) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) { + self.context = context + self.peerId = peerId + self.cancel = cancel + self.openPeer = openPeer + self.approveRequest = approveRequest + self.denyRequest = denyRequest + self.pushController = pushController + self.dismissInput = dismissInput + self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal in + if value { + return .single(value) |> delay(0.2, queue: Queue.mainQueue()) + } else { + return .single(value) + } + }).start(next: { [weak self] value in + self?.updateActivity?(value) + })) + } + + deinit { + self.activityDisposable.dispose() + } + + func isEqual(to: ItemListControllerSearch) -> Bool { + if let to = to as? InviteRequestsSearchItem { + if self.context !== to.context { + return false + } + if self.peerId != to.peerId { + return false + } + return true + } else { + return false + } + } + + func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if let current = current as? SearchNavigationContentNode { + current.updateTheme(presentationData.theme) + return current + } else { + return SearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel, updateActivity: { [weak self] value in + self?.updateActivity = value + }) + } + } + + func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode { + return InviteRequestsSearchItemNode(context: self.context, peerId: self.peerId, openPeer: self.openPeer, approveRequest: self.approveRequest, denyRequest: self.denyRequest, cancel: self.cancel, updateActivity: { [weak self] value in + self?.activity.set(value) + }, pushController: { [weak self] c in + self?.pushController(c) + }, dismissInput: self.dismissInput) + } +} + +private final class InviteRequestsSearchItemNode: ItemListControllerSearchNode { + private let containerNode: InviteRequestsSearchContainerNode + + init(context: AccountContext, peerId: PeerId, openPeer: @escaping (EnginePeer) -> Void, approveRequest: @escaping (EnginePeer) -> Void, denyRequest: @escaping (EnginePeer) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) { + self.containerNode = InviteRequestsSearchContainerNode(context: context, forceTheme: nil, peerId: peerId, openPeer: { peer in + openPeer(peer) + }, approveRequest: { peer in + approveRequest(peer) + }, denyRequest: { peer in + denyRequest(peer) + }, updateActivity: updateActivity, pushController: pushController) + self.containerNode.cancel = { + cancel() + } + + super.init() + + self.addSubnode(self.containerNode) + + self.containerNode.dismissInput = { + dismissInput() + } + } + + override func queryUpdated(_ query: String) { + self.containerNode.searchTextUpdated(text: query) + } + + override func scrollToTop() { + self.containerNode.scrollToTop() + } + + override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) + self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) { + return result + } + + return super.hitTest(point, with: event) + } +} + + +private final class InviteRequestsSearchContainerInteraction { + let openPeer: (EnginePeer) -> Void + let approveRequest: (EnginePeer) -> Void + let denyRequest: (EnginePeer) -> Void + let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?) -> Void + + init(openPeer: @escaping (EnginePeer) -> Void, approveRequest: @escaping (EnginePeer) -> Void, denyRequest: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) { + self.openPeer = openPeer + self.approveRequest = approveRequest + self.denyRequest = denyRequest + self.peerContextAction = peerContextAction + } +} + +private enum InviteRequestsSearchEntryId: Hashable { + case request(EnginePeer.Id) +} + +private final class InviteRequestsSearchEntry: Comparable, Identifiable { + let index: Int + let request: PeerInvitationImportersState.Importer + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let isGroup: Bool + + init(index: Int, request: PeerInvitationImportersState.Importer, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, isGroup: Bool) { + self.index = index + self.request = request + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.isGroup = isGroup + } + + var stableId: InviteRequestsSearchEntryId { + return .request(self.request.peer.peerId) + } + + static func ==(lhs: InviteRequestsSearchEntry, rhs: InviteRequestsSearchEntry) -> Bool { + return lhs.index == rhs.index && lhs.request == rhs.request && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.nameDisplayOrder == rhs.nameDisplayOrder && lhs.isGroup == rhs.isGroup + } + + static func <(lhs: InviteRequestsSearchEntry, rhs: InviteRequestsSearchEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: InviteRequestsSearchContainerInteraction) -> ListViewItem { + return ItemListInviteRequestItem(context: context, presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, importer: self.request, isGroup: self.isGroup, sectionId: 0, style: .plain, tapAction: { + if let peer = self.request.peer.peer.flatMap({ EnginePeer($0) }) { + interaction.openPeer(peer) + } + }, addAction: { + if let peer = self.request.peer.peer.flatMap({ EnginePeer($0) }) { + interaction.approveRequest(peer) + } + }, dismissAction: { + if let peer = self.request.peer.peer.flatMap({ EnginePeer($0) }) { + interaction.denyRequest(peer) + } + }, contextAction: { node, gesture in + if let peer = self.request.peer.peer.flatMap({ EnginePeer($0) }) { + interaction.peerContextAction(peer, node, gesture) + } + }) + } +} + +struct InviteRequestsSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool + let isEmpty: Bool + let query: String +} + +private func InviteRequestsSearchContainerPreparedRecentTransition(from fromEntries: [InviteRequestsSearchEntry], to toEntries: [InviteRequestsSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: InviteRequestsSearchContainerInteraction) -> InviteRequestsSearchContainerTransition { + 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(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) } + + return InviteRequestsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query) +} + + +public final class InviteRequestsSearchContainerNode: SearchDisplayControllerContentNode { + private let context: AccountContext + private let openPeer: (EnginePeer) -> Void + + private let dimNode: ASDisplayNode + private let listNode: ListView + + private let emptyResultsTitleNode: ImmediateTextNode + private let emptyResultsTextNode: ImmediateTextNode + + private var enqueuedTransitions: [(InviteRequestsSearchContainerTransition, Bool)] = [] + private var validLayout: (ContainerViewLayout, CGFloat)? + + private let searchQuery = Promise() + private let emptyQueryDisposable = MetaDisposable() + private let searchDisposable = MetaDisposable() + + private let forceTheme: PresentationTheme? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let presentationDataPromise: Promise + + private var _hasDim: Bool = false + override public var hasDim: Bool { + return _hasDim + } + + private var processedPeerIdsPromise = ValuePromise>(Set()) + private var processedPeerIds = Set() { + didSet { + self.processedPeerIdsPromise.set(self.processedPeerIds) + } + } + + public init(context: AccountContext, forceTheme: PresentationTheme?, peerId: PeerId, openPeer: @escaping (EnginePeer) -> Void, approveRequest: @escaping (EnginePeer) -> Void, denyRequest: @escaping (EnginePeer) -> Void, updateActivity: @escaping (Bool) -> Void, pushController: @escaping (ViewController) -> Void) { + self.context = context + self.openPeer = openPeer + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + + self.forceTheme = forceTheme + if let forceTheme = self.forceTheme { + self.presentationData = self.presentationData.withUpdated(theme: forceTheme) + } + self.presentationDataPromise = Promise(self.presentationData) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + self.emptyResultsTitleNode = ImmediateTextNode() + self.emptyResultsTitleNode.displaysAsynchronously = false + self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) + self.emptyResultsTitleNode.textAlignment = .center + self.emptyResultsTitleNode.isHidden = true + + self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.displaysAsynchronously = false + self.emptyResultsTextNode.maximumNumberOfLines = 0 + self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.isHidden = true + + super.init() + + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + self._hasDim = true + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + self.addSubnode(self.emptyResultsTitleNode) + self.addSubnode(self.emptyResultsTextNode) + + + let interaction = InviteRequestsSearchContainerInteraction(openPeer: { [weak self] peer in + openPeer(peer) + self?.listNode.clearHighlightAnimated(true) + }, approveRequest: { [weak self] peer in + approveRequest(peer) + self?.processedPeerIds.insert(peer.id) + }, denyRequest: { [weak self] peer in + denyRequest(peer) + self?.processedPeerIds.insert(peer.id) + }, peerContextAction: { _, _, _ in + + }) + + let presentationDataPromise = self.presentationDataPromise + + let previousRequestsContext = Atomic(value: nil) + + let processedPeerIds = self.processedPeerIdsPromise + let searchQuery = self.searchQuery.get() + |> mapToSignal { query -> Signal in + if let query = query, !query.isEmpty { + return (.complete() |> delay(0.6, queue: Queue.mainQueue())) + |> then(.single(query)) + } else { + return .single(query) + } + } + + let foundItems = combineLatest(searchQuery, context.account.postbox.peerView(id: peerId) |> take(1)) + |> mapToSignal { query, peerView -> Signal<[InviteRequestsSearchEntry]?, NoError> in + guard let query = query, !query.isEmpty else { + return .single(nil) + } + updateActivity(true) + let requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: query)) + let _ = previousRequestsContext.swap(requestsContext) + + return combineLatest(requestsContext.state, presentationDataPromise.get(), processedPeerIds.get()) + |> mapToSignal { state, presentationData, processedPeerIds -> Signal<[InviteRequestsSearchEntry]?, NoError> in + if !state.hasLoadedOnce { + return .complete() + } + + var entries: [InviteRequestsSearchEntry] = [] + var index = 0 + for importer in state.importers { + if processedPeerIds.contains(importer.peer.peerId) { + continue + } + entries.append(InviteRequestsSearchEntry(index: index, request: importer, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, isGroup: false)) + index += 1 + } + return .single(entries) + } + } + + let previousSearchItems = Atomic<[InviteRequestsSearchEntry]?>(value: nil) + + self.searchDisposable.set((combineLatest(searchQuery, foundItems, self.presentationDataPromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] query, entries, presentationData in + if let strongSelf = self { + let previousEntries = previousSearchItems.swap(entries) + updateActivity(false) + let firstTime = previousEntries == nil + let transition = InviteRequestsSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + var presentationData = presentationData + + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + if let forceTheme = strongSelf.forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] _ in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + override public func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override public func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: InviteRequestsSearchContainerTransition, firstTime: Bool) { + self.enqueuedTransitions.append((transition, firstTime)) + + if let _ = self.validLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, _) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + options.insert(.PreferSynchronousResourceLoading) + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + + strongSelf.listNode.isHidden = !isSearching + strongSelf.dimNode.isHidden = transition.isSearching + + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + + let emptyResults = transition.isSearching && transition.isEmpty + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + + if let (layout, navigationBarHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + }) + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let hadValidLayout = self.validLayout == nil + self.validLayout = (layout, navigationBarHeight) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let padding: CGFloat = 16.0 + let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let emptyTextSpacing: CGFloat = 8.0 + let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) + + transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) + transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + + if !hadValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + override public func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = self.view.hitTest(point, with: event) else { + return nil + } + if result === self.view { + return nil + } + return result + } +} diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift index d8c4a917b1..35d2ea8fd8 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift @@ -14,6 +14,10 @@ import LocalizedPeerData import AvatarNode import AccountContext import SolidRoundedButtonNode +import PeerInfoAvatarListNode +import ContextUI + +private let backgroundCornerRadius: CGFloat = 14.0 public class ItemListInviteRequestItem: ListViewItem, ItemListItem { let context: AccountContext @@ -130,24 +134,30 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private let maskNode: ASImageNode - - private let extractedBackgroundImageNode: ASImageNode private let containerNode: ContextControllerSourceNode private let contextSourceNode: ContextExtractedContentContainingNode - + private let extractedBackgroundImageNode: ASImageNode + private let offsetContainerNode: ASDisplayNode + private var extractedRect: CGRect? private var nonExtractedRect: CGRect? - - private let offsetContainerNode: ASDisplayNode + private var extractedVerticalOffset: CGFloat? fileprivate let avatarNode: AvatarNode + private let contentWrapperNode: ASDisplayNode private let titleNode: TextNode private let subtitleNode: TextNode + private let expandedSubtitleNode: TextNode private let dateNode: TextNode private let addButton: SolidRoundedButtonNode private let dismissButton: HighlightableButtonNode + private var avatarTransitionNode: ASImageNode? + private var avatarListContainerNode: ASDisplayNode? + private var avatarListWrapperNode: PinchSourceContainerNode? + private var avatarListNode: PeerInfoAvatarListContainerNode? + private var placeholderNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? @@ -155,6 +165,8 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { public var tag: ItemListItemTag? + private var isExtracted = false + public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -185,6 +197,11 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { self.subtitleNode.contentMode = .left self.subtitleNode.contentsScale = UIScreen.main.scale + self.expandedSubtitleNode = TextNode() + self.expandedSubtitleNode.isUserInteractionEnabled = false + self.expandedSubtitleNode.contentMode = .left + self.expandedSubtitleNode.contentsScale = UIScreen.main.scale + self.dateNode = TextNode() self.dateNode.isUserInteractionEnabled = false self.dateNode.contentMode = .left @@ -198,6 +215,8 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { self.avatarNode = AvatarNode(font: avatarFont) + self.contentWrapperNode = ASDisplayNode() + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.isAccessibilityElement = true @@ -209,12 +228,14 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) - self.offsetContainerNode.addSubnode(self.avatarNode) - self.offsetContainerNode.addSubnode(self.titleNode) - self.offsetContainerNode.addSubnode(self.subtitleNode) - self.offsetContainerNode.addSubnode(self.dateNode) - self.offsetContainerNode.addSubnode(self.addButton) - self.offsetContainerNode.addSubnode(self.dismissButton) + self.offsetContainerNode.addSubnode(self.contentWrapperNode) + self.contentWrapperNode.addSubnode(self.avatarNode) + self.contentWrapperNode.addSubnode(self.titleNode) + self.contentWrapperNode.addSubnode(self.subtitleNode) + self.contentWrapperNode.addSubnode(self.expandedSubtitleNode) + self.contentWrapperNode.addSubnode(self.dateNode) + self.contentWrapperNode.addSubnode(self.addButton) + self.contentWrapperNode.addSubnode(self.dismissButton) self.addButton.pressed = { [weak self] in if let (item, _, _, _, _) = self?.layoutParams { @@ -230,33 +251,241 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { } contextAction(strongSelf.contextSourceNode, gesture) } - + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in - guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else { + guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let peer = item.importer?.peer.peer else { return } + strongSelf.isExtracted = isExtracted + + let inset: CGFloat = 0.0 if isExtracted { - strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.plainBackgroundColor) - } - - if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { - let rect = isExtracted ? extractedRect : nonExtractedRect - transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) - } - - transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) - transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in - if !isExtracted { - self?.extractedBackgroundImageNode.image = nil + strongSelf.contextSourceNode.contentNode.customHitTest = { [weak self] point in + if let strongSelf = self { + if let avatarListWrapperNode = strongSelf.avatarListWrapperNode, avatarListWrapperNode.frame.contains(point) { + return strongSelf.avatarListNode?.view + } + } + return nil } - }) + } else { + strongSelf.contextSourceNode.contentNode.customHitTest = nil + } + + let extractedVerticalOffset = strongSelf.extractedVerticalOffset ?? 0.0 + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect: CGRect + if isExtracted { + if extractedVerticalOffset > 0.0 { + rect = CGRect(x: extractedRect.minX, y: extractedRect.minY + extractedVerticalOffset, width: extractedRect.width, height: extractedRect.height - extractedVerticalOffset) + } else { + rect = extractedRect + } + } else { + rect = nonExtractedRect + } + + let springDuration: Double = isExtracted ? 0.42 : 0.3 + let springDamping: CGFloat = isExtracted ? 124.0 : 1000.0 + + let itemBackgroundColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + } + + if !extractedVerticalOffset.isZero { + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(itemBackgroundColor.cgColor) + context.fillEllipse(in: bounds) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) + + strongSelf.avatarNode.transform = CATransform3DIdentity + var avatarInitialRect = strongSelf.avatarNode.view.convert(strongSelf.avatarNode.bounds, to: strongSelf.offsetContainerNode.supernode?.view) + if strongSelf.avatarTransitionNode == nil { + let targetRect = CGRect(x: extractedRect.minX, y: extractedRect.minY, width: extractedRect.width, height: extractedRect.width) + let initialScale = avatarInitialRect.width / targetRect.width + avatarInitialRect.origin.y += backgroundCornerRadius / 2.0 * initialScale + + let avatarListWrapperNode = PinchSourceContainerNode() + avatarListWrapperNode.clipsToBounds = true + avatarListWrapperNode.cornerRadius = backgroundCornerRadius + avatarListWrapperNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode?.controlsContainerNode.alpha = 0.0 + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + item.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + avatarListWrapperNode.deactivated = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListWrapperNode?.contentNode.layer.animate(from: 0.0 as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, completion: { _ in + }) + } + avatarListWrapperNode.update(size: targetRect.size, transition: .immediate) + avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.height + backgroundCornerRadius) + avatarListWrapperNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode?.controlsContainerNode.alpha = 1.0 + strongSelf.avatarListNode?.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + let transitionNode = ASImageNode() + transitionNode.clipsToBounds = true + transitionNode.displaysAsynchronously = false + transitionNode.displayWithoutProcessing = true + transitionNode.image = strongSelf.avatarNode.unroundedImage + transitionNode.frame = CGRect(origin: CGPoint(), size: targetRect.size) + transitionNode.cornerRadius = targetRect.width / 2.0 + radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: 0.0) + + strongSelf.avatarNode.isHidden = true + avatarListWrapperNode.contentNode.addSubnode(transitionNode) + + strongSelf.avatarTransitionNode = transitionNode + + let avatarListContainerNode = ASDisplayNode() + avatarListContainerNode.clipsToBounds = true + avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetRect.size) + avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + avatarListContainerNode.cornerRadius = targetRect.width / 2.0 + + avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarInitialRect.center), to: NSValue(cgPoint: avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: 0.0) + + let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context) + avatarListWrapperNode.contentNode.clipsToBounds = true + avatarListNode.backgroundColor = .clear + avatarListNode.peer = peer + avatarListNode.firstFullSizeOnly = true + avatarListNode.offsetLocation = true + avatarListNode.customCenterTapAction = { [weak self] in + self?.contextSourceNode.requestDismiss?() + } + avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.height / 2.0, width: targetRect.width, height: targetRect.height) + avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.height / 2.0, width: targetRect.width, height: targetRect.height) + avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.height / 2.0), size: CGSize()) + avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) + avatarListNode.shadowNode.frame = CGRect(x: 0.0, y: 0.0, width: targetRect.width, height: 44.0) + + avatarListContainerNode.addSubnode(avatarListNode) + avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode) + avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode) + + avatarListNode.update(size: targetRect.size, peer: peer, customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate) + strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode) + + strongSelf.avatarListWrapperNode = avatarListWrapperNode + strongSelf.avatarListContainerNode = avatarListContainerNode + strongSelf.avatarListNode = avatarListNode + } + } else if let transitionNode = strongSelf.avatarTransitionNode, let avatarListWrapperNode = strongSelf.avatarListWrapperNode, let avatarListContainerNode = strongSelf.avatarListContainerNode { + + var avatarInitialRect = CGRect(origin: strongSelf.avatarNode.frame.origin, size: strongSelf.avatarNode.frame.size) + let targetScale = avatarInitialRect.width / avatarListContainerNode.frame.width + avatarInitialRect.origin.y += backgroundCornerRadius / 2.0 * targetScale + + strongSelf.avatarTransitionNode = nil + strongSelf.avatarListWrapperNode = nil + strongSelf.avatarListContainerNode = nil + strongSelf.avatarListNode = nil + + avatarListContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak avatarListContainerNode] _ in + avatarListContainerNode?.removeFromSupernode() + }) + + avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) + avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: avatarListWrapperNode.position), to: NSValue(cgPoint: avatarInitialRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionNode, weak self] _ in + transitionNode?.removeFromSupernode() + self?.avatarNode.isHidden = false + }) + + radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: avatarListContainerNode.frame.width / 2.0) + radiusTransition.updateCornerRadius(node: transitionNode, cornerRadius: avatarListContainerNode.frame.width / 2.0) + } + + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + alphaTransition.updateAlpha(node: strongSelf.subtitleNode, alpha: isExtracted ? 0.0 : 1.0) + alphaTransition.updateAlpha(node: strongSelf.expandedSubtitleNode, alpha: isExtracted ? 1.0 : 0.0) + alphaTransition.updateAlpha(node: strongSelf.dateNode, alpha: isExtracted ? 0.0 : 1.0) + alphaTransition.updateAlpha(node: strongSelf.addButton, alpha: isExtracted ? 0.0 : 1.0, delay: isExtracted ? 0.0 : 0.1) + alphaTransition.updateAlpha(node: strongSelf.dismissButton, alpha: isExtracted ? 0.0 : 1.0, delay: isExtracted ? 0.0 : 0.1) + + let offsetInitialSublayerTransform = strongSelf.offsetContainerNode.layer.sublayerTransform + strongSelf.offsetContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? -43 : 0.0, isExtracted ? extractedVerticalOffset : 0.0, 0.0) + + let initialExtractedBackgroundPosition = strongSelf.extractedBackgroundImageNode.position + strongSelf.extractedBackgroundImageNode.layer.position = rect.center + let initialExtractedBackgroundBounds = strongSelf.extractedBackgroundImageNode.bounds + strongSelf.extractedBackgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: rect.size) + if isExtracted { + strongSelf.offsetContainerNode.layer.animateSpring(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialExtractedBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + strongSelf.extractedBackgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialExtractedBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } else { + strongSelf.offsetContainerNode.layer.animate(from: NSValue(caTransform3D: offsetInitialSublayerTransform), to: NSValue(caTransform3D: strongSelf.offsetContainerNode.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgPoint: initialExtractedBackgroundPosition), to: NSValue(cgPoint: strongSelf.extractedBackgroundImageNode.position), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + strongSelf.extractedBackgroundImageNode.layer.animate(from: NSValue(cgRect: initialExtractedBackgroundBounds), to: NSValue(cgRect: strongSelf.extractedBackgroundImageNode.bounds), keyPath: "bounds", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.alpha = 1.0 + strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.1, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + } else { + strongSelf.extractedBackgroundImageNode.alpha = 0.0 + strongSelf.extractedBackgroundImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.extractedBackgroundImageNode.image = nil + strongSelf.extractedBackgroundImageNode.layer.removeAllAnimations() + } + }) + } + } else { + if isExtracted { + strongSelf.extractedBackgroundImageNode.alpha = 1.0 + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: backgroundCornerRadius * 2.0, color: item.presentationData.theme.list.itemBlocksBackgroundColor) + } + + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: CGRect(origin: CGPoint(), size: rect.size)) + + transition.updateAlpha(node: strongSelf.subtitleNode, alpha: isExtracted ? 0.0 : 1.0) + transition.updateAlpha(node: strongSelf.expandedSubtitleNode, alpha: isExtracted ? 1.0 : 0.0) + + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? inset : 0.0, y: isExtracted ? extractedVerticalOffset : 0.0)) + +// transition.updateAlpha(node: strongSelf.backgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in +// if !isExtracted { +// self?.backgroundImageNode.image = nil +// self?.extractedBackgroundImageNode.image = nil +// } +// }) + } + } } } public func asyncLayout() -> (_ item: ItemListInviteRequestItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeExpandedSubtitleLayout = TextNode.asyncLayout(self.expandedSubtitleNode) let makeDateLayout = TextNode.asyncLayout(self.dateNode) let currentItem = self.layoutParams?.0 @@ -296,6 +525,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (expandedSubtitleLayout, expandedSubtitleApply) = makeExpandedSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = 1.0 @@ -313,7 +543,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor insets = itemListNeighborsPlainInsets(neighbors) - insets.top = firstWithHeader ? 29.0 : 0.0 + insets.top = 0.0 insets.bottom = 0.0 case .blocks: itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor @@ -336,11 +566,23 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contentWrapperNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.containerNode.isGestureEnabled = item.contextAction != nil - let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) - let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + let nonExtractedRect = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: layout.contentSize.width - 32.0, height: layout.contentSize.height)) + var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: params.leftInset, dy: 0.0) + var extractedHeight = extractedRect.height + expandedSubtitleLayout.size.height - subtitleLayout.size.height + var extractedVerticalOffset: CGFloat = 0.0 + if item.importer?.peer.peer?.smallProfileImage != nil { + extractedRect.size.width = min(extractedRect.width, params.availableHeight - 20.0) + extractedVerticalOffset = extractedRect.width + extractedHeight += extractedVerticalOffset + } + + extractedRect.size.height = extractedHeight - 48.0 + + strongSelf.extractedVerticalOffset = extractedVerticalOffset strongSelf.extractedRect = extractedRect strongSelf.nonExtractedRect = nonExtractedRect @@ -362,6 +604,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { let _ = titleApply() let _ = subtitleApply() + let _ = expandedSubtitleApply() let _ = dateApply() switch item.style { @@ -439,6 +682,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + transition.updateFrame(node: strongSelf.expandedSubtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: expandedSubtitleLayout.size)) transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - rightInset - dateLayout.size.width, y: verticalInset + 2.0), size: dateLayout.size)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index c6dadf4a31..141c7d9171 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -66,7 +66,7 @@ public final class JoinLinkPreviewController: ViewController { } self.displayNodeDidLoad() - let signal: Signal + let signal: Signal if let resolvedState = self.resolvedState { signal = .single(resolvedState) } else { @@ -99,6 +99,16 @@ public final class JoinLinkPreviewController: ViewController { strongSelf.dismiss() } } + }, error: { [weak self] error in + if let strongSelf = self { + switch error { + case .flood: + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_FloodError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + default: + break + } + strongSelf.dismiss() + } })) self.ready.set(self.controllerNode.ready.get()) } diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index 197f9bfa32..5514efa80c 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -124,7 +124,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi self.wrappingScrollNode.addSubnode(self.contentContainerNode) self.wrappingScrollNode.addSubnode(self.cancelButton) - self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false)) + self.transitionToContentNode(JoinLinkPreviewLoadingContainerNode(theme: self.presentationData.theme)) self.ready.set(.single(true)) self.didSetReady = true @@ -184,7 +184,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi } contentNode.layer.add(animation, forKey: "opacity") - self.animateContentNodeOffsetFromBackgroundOffset = self.contentBackgroundNode.frame.minY + self.animateContentNodeOffsetFromBackgroundOffset = self.backgroundNode.frame.minY self.scheduleInteractiveTransition(transition) contentNode.activate() @@ -394,20 +394,6 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi self.setNeedsLayout() } - func transitionToProgress(signal: Signal) { - self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: false), fastOut: true) - let timestamp = CACurrentMediaTime() - self.disposable.set(signal.start(completed: { [weak self] in - let minDelay = 0.6 - let delay = max(0.0, (timestamp + minDelay) - CACurrentMediaTime()) - Queue.mainQueue().after(delay, { - if let strongSelf = self { - strongSelf.cancel?() - } - }) - })) - } - func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, memberCount: memberCount, members: members)) contentNode.join = { [weak self] in diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index b9c074909d..22dea7e9c7 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -9,6 +9,7 @@ import AccountContext import SelectablePeerNode import ShareController import SolidRoundedButtonNode +import ActivityIndicator private let avatarFont = avatarPlaceholderFont(size: 42.0) @@ -250,3 +251,51 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer func updateSelectedPeers() { } } + +public enum ShareLoadingState { + case preparing + case progress(Float) + case done +} + +public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareContentContainerNode { + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + private let theme: PresentationTheme + private let activityIndicator: ActivityIndicator + + public init(theme: PresentationTheme) { + self.theme = theme + self.activityIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, 22.0, 2.0, false)) + + super.init() + + self.addSubnode(self.activityIndicator) + } + + public func activate() { + } + + public func deactivate() { + } + + public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { + } + + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let nodeHeight: CGFloat = 125.0 + + let indicatorSize = self.activityIndicator.calculateSizeThatFits(size) + let indicatorFrame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: size.height - nodeHeight + floor((nodeHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + transition.updateFrame(node: self.activityIndicator, frame: indicatorFrame) + + self.contentOffsetUpdated?(-size.height + nodeHeight, transition) + } + + public func updateSelectedPeers() { + } +} diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index 77cdaadbca..27ffe4f567 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -17,19 +17,27 @@ final class LocationInfoListItem: ListViewItem { let location: TelegramMediaMap let address: String? let distance: String? - let eta: String? + let drivingTime: Double? + let transitTime: Double? + let walkingTime: Double? let action: () -> Void - let getDirections: () -> Void + let drivingAction: () -> Void + let transitAction: () -> Void + let walkingAction: () -> Void - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, eta: String?, action: @escaping () -> Void, getDirections: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, drivingTime: Double?, transitTime: Double?, walkingTime: Double?, action: @escaping () -> Void, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { self.presentationData = presentationData self.engine = engine self.location = location self.address = address self.distance = distance - self.eta = eta + self.drivingTime = drivingTime + self.transitTime = transitTime + self.walkingTime = walkingTime self.action = action - self.getDirections = getDirections + self.drivingAction = drivingAction + self.transitAction = transitAction + self.walkingAction = walkingAction } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -71,7 +79,10 @@ final class LocationInfoListItemNode: ListViewItemNode { private var subtitleNode: TextNode? private let venueIconNode: TransformImageNode private let buttonNode: HighlightableButtonNode - private var directionsButtonNode: SolidRoundedButtonNode? + + private var drivingButtonNode: SolidRoundedButtonNode? + private var transitButtonNode: SolidRoundedButtonNode? + private var walkingButtonNode: SolidRoundedButtonNode? private var item: LocationInfoListItem? private var layoutParams: ListViewItemLayoutParams? @@ -166,7 +177,7 @@ final class LocationInfoListItemNode: ListViewItemNode { let titleSpacing: CGFloat = 1.0 let bottomInset: CGFloat = 4.0 - let contentSize = CGSize(width: params.width, height: max(126.0, verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset)) + let contentSize = CGSize(width: params.width, height: max(100.0, verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset)) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) return (nodeLayout, { [weak self] in @@ -187,7 +198,6 @@ final class LocationInfoListItemNode: ListViewItemNode { if let _ = updatedTheme { strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - strongSelf.directionsButtonNode?.updateTheme(SolidRoundedButtonTheme(theme: item.presentationData.theme)) } let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) @@ -212,19 +222,50 @@ final class LocationInfoListItemNode: ListViewItemNode { strongSelf.addSubnode(subtitleNode) } - let directionsButtonNode: SolidRoundedButtonNode - if let currentDirectionsButtonNode = strongSelf.directionsButtonNode { - directionsButtonNode = currentDirectionsButtonNode - } else { - directionsButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: item.presentationData.theme), height: 50.0, cornerRadius: 10.0) - directionsButtonNode.title = item.presentationData.strings.Map_Directions - directionsButtonNode.pressed = { - item.getDirections() + let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) + if strongSelf.drivingButtonNode == nil { + strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.drivingButtonNode?.iconSpacing = 5.0 + strongSelf.drivingButtonNode?.alpha = 0.0 + strongSelf.drivingButtonNode?.allowsGroupOpacity = true + strongSelf.drivingButtonNode?.pressed = { [weak self] in + if let item = self?.item { + item.drivingAction() + } } - strongSelf.addSubnode(directionsButtonNode) - strongSelf.directionsButtonNode = directionsButtonNode + strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } + + strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.transitButtonNode?.iconSpacing = 2.0 + strongSelf.transitButtonNode?.alpha = 0.0 + strongSelf.transitButtonNode?.allowsGroupOpacity = true + strongSelf.transitButtonNode?.pressed = { [weak self] in + if let item = self?.item { + item.transitAction() + } + } + strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } + + strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.walkingButtonNode?.iconSpacing = 2.0 + strongSelf.walkingButtonNode?.alpha = 0.0 + strongSelf.walkingButtonNode?.allowsGroupOpacity = true + strongSelf.walkingButtonNode?.pressed = { [weak self] in + if let item = self?.item { + item.walkingAction() + } + } + strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } + } else if let _ = updatedTheme { + strongSelf.drivingButtonNode?.updateTheme(buttonTheme) + strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + + strongSelf.transitButtonNode?.updateTheme(buttonTheme) + strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + + strongSelf.walkingButtonNode?.updateTheme(buttonTheme) + strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) } - directionsButtonNode.subtitle = item.eta let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame @@ -235,9 +276,42 @@ final class LocationInfoListItemNode: ListViewItemNode { let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize)) strongSelf.venueIconNode.frame = iconNodeFrame - let directionsWidth = contentSize.width - inset * 2.0 - let directionsHeight = directionsButtonNode.updateLayout(width: directionsWidth, transition: .immediate) - directionsButtonNode.frame = CGRect(x: inset, y: iconNodeFrame.maxY + 14.0, width: directionsWidth, height: directionsHeight) + if let drivingTime = item.drivingTime { + strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) + + if currentItem?.drivingTime == nil { + strongSelf.drivingButtonNode?.alpha = 1.0 + strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + if let transitTime = item.transitTime { + strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 }) + + if currentItem?.transitTime == nil { + strongSelf.transitButtonNode?.alpha = 1.0 + strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + if let walkingTime = item.walkingTime { + strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) + + if currentItem?.walkingTime == nil { + strongSelf.walkingButtonNode?.alpha = 1.0 + strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + let directionsWidth: CGFloat = 93.0 + let directionsSpacing: CGFloat = 8.0 + let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + + strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight)) + strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset + directionsWidth + directionsSpacing, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight)) + strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: leftInset + directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight)) strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) diff --git a/submodules/LocationUI/Sources/LocationLiveListItem.swift b/submodules/LocationUI/Sources/LocationLiveListItem.swift index 7dd222b5cd..0e81e1c9d4 100644 --- a/submodules/LocationUI/Sources/LocationLiveListItem.swift +++ b/submodules/LocationUI/Sources/LocationLiveListItem.swift @@ -242,7 +242,7 @@ final class LocationLiveListItemNode: ListViewItemNode { let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) if strongSelf.drivingButtonNode == nil { - strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsDriving"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) strongSelf.drivingButtonNode?.alpha = 0.0 strongSelf.drivingButtonNode?.allowsGroupOpacity = true strongSelf.drivingButtonNode?.pressed = { [weak self] in @@ -252,7 +252,7 @@ final class LocationLiveListItemNode: ListViewItemNode { } strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } - strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsTransit"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) strongSelf.transitButtonNode?.alpha = 0.0 strongSelf.transitButtonNode?.allowsGroupOpacity = true strongSelf.transitButtonNode?.pressed = { [weak self] in @@ -262,7 +262,7 @@ final class LocationLiveListItemNode: ListViewItemNode { } strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } - strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: UIImage(bundleImageName: "Location/DirectionsWalking"), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) + strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) strongSelf.walkingButtonNode?.alpha = 0.0 strongSelf.walkingButtonNode?.allowsGroupOpacity = true strongSelf.walkingButtonNode?.pressed = { [weak self] in @@ -273,8 +273,13 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } } else if let _ = updatedTheme { strongSelf.drivingButtonNode?.updateTheme(buttonTheme) + strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + strongSelf.transitButtonNode?.updateTheme(buttonTheme) + strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + strongSelf.walkingButtonNode?.updateTheme(buttonTheme) + strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) } let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 09eb9ad150..0f4c6bf1f3 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -47,7 +47,7 @@ private enum LocationViewEntryId: Hashable { } private enum LocationViewEntry: Comparable, Identifiable { - case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?) + case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?, Double?, Double?) case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?) case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, Message, Double?, Double?, Double?, Double?, Int) @@ -64,8 +64,8 @@ private enum LocationViewEntry: Comparable, Identifiable { static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { switch lhs { - case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsTime): - if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsTime) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsTime == rhsTime { + case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime): + if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime { return true } else { return false @@ -113,7 +113,7 @@ private enum LocationViewEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem { switch self { - case let .info(_, location, address, distance, time): + case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime): let addressString: String? if let address = address { addressString = address @@ -126,14 +126,14 @@ private enum LocationViewEntry: Comparable, Identifiable { } else { distanceString = nil } - var eta: String? - if let time = time, time < 60.0 * 60.0 * 10.0 { - eta = stringForEstimatedDuration(strings: presentationData.strings, time: time, format: { presentationData.strings.Map_DirectionsDriveEta($0).string }) - } - return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, eta: eta, action: { + return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { interaction?.goToCoordinate(location.coordinate) - }, getDirections: { + }, drivingAction: { interaction?.requestDirections(location, nil, .driving) + }, transitAction: { + interaction?.requestDirections(location, nil, .transit) + }, walkingAction: { + interaction?.requestDirections(location, nil, .walking) }) case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout): let beginTimeAndTimeout: (Double, Double)? @@ -286,12 +286,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan throttledUserLocation(self.headerNode.mapNode.userLocation) ) - var eta: Signal = .single(nil) + var eta: Signal<(Double?, Double?, Double?)?, NoError> = .single(nil) var address: Signal = .single(nil) if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil { eta = .single(nil) - |> then(getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) |> map(Optional.init)) if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { address = .single(venueAddress) @@ -359,7 +359,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } - entries.append(.info(presentationData.theme, location, address, distance, eta)) + entries.append(.info(presentationData.theme, location, address, distance, eta?.0, eta?.1, eta?.2)) annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: location, forcedSelection: true)) } else { @@ -814,7 +814,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } let overlap: CGFloat = 6.0 - var topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - 126.0 - overlap + var topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - 100.0 - overlap if let location = getLocation(from: self.subject), location.liveBroadcastingTimeout != nil { topInset += 66.0 } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 36fb8366b1..14d005336e 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -77,7 +77,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case publicLinkAvailability(PresentationTheme, String, Bool) case editablePublicLink(PresentationTheme, PresentationStrings, String, String) case privateLinkHeader(PresentationTheme, String) - case privateLink(PresentationTheme, ExportedInvitation?, Bool) + case privateLink(PresentationTheme, ExportedInvitation?, [EnginePeer], Int32, Bool) case privateLinkInfo(PresentationTheme, String) case privateLinkManage(PresentationTheme, String) case privateLinkManageInfo(PresentationTheme, String) @@ -182,8 +182,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } - case let .privateLink(lhsTheme, lhsInvite, lhsDisplayImporters): - if case let .privateLink(rhsTheme, rhsInvite, rhsDisplayImporters) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, lhsDisplayImporters == rhsDisplayImporters { + case let .privateLink(lhsTheme, lhsInvite, lhsPeers, lhsImportersCount, lhsDisplayImporters): + if case let .privateLink(rhsTheme, rhsInvite, rhsPeers, rhsImportersCount, rhsDisplayImporters) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, lhsPeers == rhsPeers, lhsImportersCount == rhsImportersCount, lhsDisplayImporters == rhsDisplayImporters { return true } else { return false @@ -290,8 +290,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section) case let .privateLinkHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) - case let .privateLink(_, invite, displayImporters): - return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, peers: [], displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { + case let .privateLink(_, invite, peers, importersCount, displayImporters): + return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { if let invite = invite { arguments.copyLink(invite) } @@ -452,7 +452,7 @@ private struct ChannelVisibilityControllerState: Equatable { } } -private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { +private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [Peer]?, importers: PeerInvitationImportersState?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { var entries: [ChannelVisibilityEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { @@ -615,7 +615,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa case .privateChannel: let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) - entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup)) + entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, mode != .initialSetup)) if isGroup { entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) } else { @@ -634,7 +634,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa case .privateLink: let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) - entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup)) + entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, mode != .initialSetup)) entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_Help)) switch mode { case .initialSetup: @@ -733,7 +733,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa case .privateChannel: let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) - entries.append(.privateLink(presentationData.theme, invite, mode != .initialSetup)) + entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, mode != .initialSetup)) entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) switch mode { case .initialSetup: @@ -926,7 +926,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } presentControllerImpl?(shareController, nil) }, linkContextAction: { node, gesture in - guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else { + guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?() else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -1050,7 +1050,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, manageInviteLinks: { let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) @@ -1063,10 +1063,28 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta let previousHadNamesToRevoke = Atomic(value: nil) let previousInvitation = Atomic(value: nil) - let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData - let signal = combineLatest(presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue) + let mainLink = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId) + ) + + let importersState = Promise(nil) + let importersContext: Signal = mainLink + |> distinctUntilChanged |> deliverOnMainQueue - |> map { presentationData, state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { invite -> PeerInvitationImportersContext? in + return invite.flatMap { context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .invite(invite: $0, requested: false)) } + } |> afterNext { context in + if let context = context { + importersState.set(context.state |> map(Optional.init)) + } else { + importersState.set(.single(nil)) + } + } + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue, importersContext, importersState.get()) + |> deliverOnMainQueue + |> map { presentationData, state, view, publicChannelsToRevoke, importersContext, importers -> (ItemListControllerState, (ItemListNodeState, Any)) in let peer = peerViewMainPeer(view) var rightNavigationButton: ItemListNavigationButton? @@ -1302,7 +1320,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } } - let entries = channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state) + let entries = channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, importers: importers, state: state) var focusItemTag: ItemListItemTag? if entries.count > 1, let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { @@ -1439,26 +1457,16 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta return controller } -private final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource { - var keepInPlace: Bool - let ignoreContentTouches: Bool = true - let blurBackground: Bool - +final class InviteLinkContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController - private let sourceNode: ContextExtractedContentContainingNode + private let sourceNode: ContextReferenceContentNode - init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) { + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { self.controller = controller self.sourceNode = sourceNode - self.keepInPlace = false - self.blurBackground = false } - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) } } diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index fcfeea1fd8..e0c69512fb 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -6,7 +6,6 @@ import Postbox import TelegramPresentationData import ActivityIndicator import RadialStatusNode -import RadialStatusNode public enum ShareLoadingState { case preparing diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index d92751fc6a..c8b6c169b4 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -54,6 +54,20 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } } + public var icon: UIImage? { + didSet { + self.iconNode.image = icon + } + } + + public var iconSpacing: CGFloat = 8.0 { + didSet { + if let width = self.validLayout { + _ = self.updateLayout(width: width, transition: .immediate) + } + } + } + public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme self.font = font @@ -66,9 +80,6 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.buttonBackgroundNode.clipsToBounds = true self.buttonBackgroundNode.backgroundColor = theme.backgroundColor self.buttonBackgroundNode.cornerRadius = cornerRadius - if #available(iOS 13.0, *) { - self.buttonBackgroundNode.layer.cornerCurve = .continuous - } self.buttonGlossNode = SolidRoundedButtonGlossNode(color: theme.foregroundColor, cornerRadius: cornerRadius) @@ -122,6 +133,14 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } } + public override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.buttonBackgroundNode.layer.cornerCurve = .continuous + } + } + public func updateTheme(_ theme: SolidRoundedButtonTheme) { guard theme !== self.theme else { return @@ -158,7 +177,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { let iconSize = self.iconNode.image?.size ?? CGSize() let titleSize = self.titleNode.updateLayout(buttonSize) - let iconSpacing: CGFloat = 8.0 + let iconSpacing: CGFloat = self.iconSpacing var contentWidth: CGFloat = titleSize.width if !iconSize.width.isZero { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index aea40542c7..36807d7695 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -286,6 +286,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-997782967] = { return Api.Update.parse_updateBotStopped($0) } dict[192428418] = { return Api.Update.parse_updateGroupCallConnection($0) } dict[1299263278] = { return Api.Update.parse_updateBotCommands($0) } + dict[-82532135] = { return Api.Update.parse_updatePendingJoinRequests($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) } @@ -469,6 +470,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-384910503] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteEdit($0) } dict[1048537159] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantVolume($0) } dict[1855199800] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeHistoryTTL($0) } + dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) } dict[-1271602504] = { return Api.auth.ExportedAuthorization.parse_exportedAuthorization($0) } dict[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($0) } dict[569137759] = { return Api.SecurePlainData.parse_securePlainEmail($0) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 6e0f5b914c..d7f551cae5 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -4827,6 +4827,7 @@ public extension Api { case updateBotStopped(userId: Int64, date: Int32, stopped: Api.Bool, qts: Int32) case updateGroupCallConnection(flags: Int32, params: Api.DataJSON) case updateBotCommands(peer: Api.Peer, botId: Int64, commands: [Api.BotCommand]) + case updatePendingJoinRequests(peer: Api.Peer, requestsPending: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -5641,6 +5642,13 @@ public extension Api { item.serialize(buffer, true) } break + case .updatePendingJoinRequests(let peer, let requestsPending): + if boxed { + buffer.appendInt32(-82532135) + } + peer.serialize(buffer, true) + serializeInt32(requestsPending, buffer: buffer, boxed: false) + break } } @@ -5832,6 +5840,8 @@ public extension Api { return ("updateGroupCallConnection", [("flags", flags), ("params", params)]) case .updateBotCommands(let peer, let botId, let commands): return ("updateBotCommands", [("peer", peer), ("botId", botId), ("commands", commands)]) + case .updatePendingJoinRequests(let peer, let requestsPending): + return ("updatePendingJoinRequests", [("peer", peer), ("requestsPending", requestsPending)]) } } @@ -7491,6 +7501,22 @@ public extension Api { return nil } } + public static func parse_updatePendingJoinRequests(_ reader: BufferReader) -> Update? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.Update.updatePendingJoinRequests(peer: _1!, requestsPending: _2!) + } + else { + return nil + } + } } public enum PopularContact: TypeConstructorDescription { @@ -11428,6 +11454,7 @@ public extension Api { case channelAdminLogEventActionExportedInviteEdit(prevInvite: Api.ExportedChatInvite, newInvite: Api.ExportedChatInvite) case channelAdminLogEventActionParticipantVolume(participant: Api.GroupCallParticipant) case channelAdminLogEventActionChangeHistoryTTL(prevValue: Int32, newValue: Int32) + case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -11637,6 +11664,13 @@ public extension Api { serializeInt32(prevValue, buffer: buffer, boxed: false) serializeInt32(newValue, buffer: buffer, boxed: false) break + case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): + if boxed { + buffer.appendInt32(-1347021750) + } + invite.serialize(buffer, true) + serializeInt64(approvedBy, buffer: buffer, boxed: false) + break } } @@ -11706,6 +11740,8 @@ public extension Api { return ("channelAdminLogEventActionParticipantVolume", [("participant", participant)]) case .channelAdminLogEventActionChangeHistoryTTL(let prevValue, let newValue): return ("channelAdminLogEventActionChangeHistoryTTL", [("prevValue", prevValue), ("newValue", newValue)]) + case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): + return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", invite), ("approvedBy", approvedBy)]) } } @@ -12151,6 +12187,22 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionParticipantJoinByRequest(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.ExportedChatInvite? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite + } + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByRequest(invite: _1!, approvedBy: _2!) + } + else { + return nil + } + } } public enum SecurePlainData: TypeConstructorDescription { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 7224d9f0c6..7c20a72a05 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1451,6 +1451,28 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo } return current }) + case let .updatePendingJoinRequests(peer, requestsPending): + updatedState.updateCachedPeerData(peer.peerId, { current in + if peer.peerId.namespace == Namespaces.Peer.CloudGroup { + let previous: CachedGroupData + if let current = current as? CachedGroupData { + previous = current + } else { + previous = CachedGroupData() + } + return previous.withUpdatedInviteRequestsPending(requestsPending) + } else if peer.peerId.namespace == Namespaces.Peer.CloudChannel { + let previous: CachedChannelData + if let current = current as? CachedChannelData { + previous = current + } else { + previous = CachedChannelData() + } + return previous.withUpdatedInviteRequestsPending(requestsPending) + } else { + return current + } + }) default: break } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 44886b6eb6..9c33c25f16 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -64,6 +64,7 @@ public enum AdminLogEventAction { case participantJoinedViaInvite(ExportedInvitation) case changeHistoryTTL(previousValue: Int32?, updatedValue: Int32?) case changeTheme(previous: String?, updated: String?) + case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId) } public enum ChannelAdminLogEventError { @@ -250,6 +251,8 @@ func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, m action = .groupCallUpdateParticipantVolume(peerId: parsedParticipant.peerId, volume: parsedParticipant.volume ?? 10000) case let .channelAdminLogEventActionChangeHistoryTTL(prevValue, newValue): action = .changeHistoryTTL(previousValue: prevValue, updatedValue: newValue) + case let .channelAdminLogEventActionParticipantJoinByRequest(invite, approvedBy): + action = .participantJoinByRequest(invitation: ExportedInvitation(apiExportedInvite: invite), approvedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(approvedBy))) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index 4ef602b7c0..ffed92623c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -81,7 +81,7 @@ func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, ex if let _ = usageLimit { flags |= (1 << 1) } - if let _ = requestNeeded { + if let requestNeeded = requestNeeded, requestNeeded { flags |= (1 << 3) } return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index 6001129a9b..e1b5edc220 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -4,6 +4,11 @@ import TelegramApi import MtProtoKit +public enum JoinLinkInfoError { + case generic + case flood +} + public enum JoinLinkError { case generic case tooMuchJoined @@ -49,7 +54,7 @@ func _internal_joinChatInteractively(with hash: String, account: Account) -> Sig case "INVITE_REQUEST_SENT": return .requestSent default: - if error.description.hasPrefix("FLOOD_WAIT") { + if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .flood } else { return .generic @@ -74,13 +79,17 @@ func _internal_joinChatInteractively(with hash: String, account: Account) -> Sig } } -func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal { - return account.network.request(Api.functions.messages.checkChatInvite(hash: hash)) +func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal { + return account.network.request(Api.functions.messages.checkChatInvite(hash: hash), automaticFloodWait: false) |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) + |> `catch` { error -> Signal in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .fail(.flood) + } else { + return .single(nil) + } } - |> mapToSignal { (result) -> Signal in + |> mapToSignal { result -> Signal in if let result = result { switch result { case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants): @@ -96,6 +105,7 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal castError(JoinLinkInfoError.self) } return .single(.invalidHash) case let .chatInvitePeek(chat, expires): @@ -107,6 +117,7 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal castError(JoinLinkInfoError.self) } return .single(.invalidHash) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index ecccb96e0f..0e32199fee 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -486,7 +486,7 @@ public extension TelegramEngine { return _internal_joinChatInteractively(with: hash, account: self.account) } - public func joinLinkInformation(_ hash: String) -> Signal { + public func joinLinkInformation(_ hash: String) -> Signal { return _internal_joinLinkInformation(hash, account: self.account) } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index b47747522c..a41f16b80d 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -191,6 +191,7 @@ private struct ApplicationSpecificNoticeKeys { private static let inlineBotLocationRequestNamespace: Int32 = 5 private static let psaAcknowledgementNamespace: Int32 = 6 private static let botGameNoticeNamespace: Int32 = 7 + private static let peerInviteRequestsNamespace: Int32 = 8 static func inlineBotLocationRequestNotice(peerId: PeerId) -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: inlineBotLocationRequestNamespace), key: noticeKey(peerId: peerId, key: 0)) @@ -319,6 +320,10 @@ private struct ApplicationSpecificNoticeKeys { static func interactiveEmojiSyncTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.interactiveEmojiSyncTip.key) } + + static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: peerInviteRequestsNamespace), key: noticeKey(peerId: peerId, key: 0)) + } } public struct ApplicationSpecificNotice { @@ -1016,6 +1021,25 @@ public struct ApplicationSpecificNotice { } } + public static func dismissedInvitationRequests(accountManager: AccountManager, peerId: PeerId) -> Signal<[Int64]?, NoError> { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedInvitationRequestsNotice(peerId: peerId)) + |> map { view -> [Int64]? in + if let value = view.value?.get(ApplicationSpecificInt64ArrayNotice.self) { + return value.values + } else { + return nil + } + } + } + + public static func setDismissedInvitationRequests(accountManager: AccountManager, peerId: PeerId, values: [Int64]) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificInt64ArrayNotice(values: values)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedInvitationRequestsNotice(peerId: peerId), entry) + } + } + } + public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json index ea6514ba62..ca0fbf9dd6 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "car.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "car_24.pdf", + "idiom" : "universal" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png deleted file mode 100644 index ae154703b4..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car_24.pdf b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car_24.pdf new file mode 100644 index 0000000000..ba9593da34 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsDriving.imageset/car_24.pdf @@ -0,0 +1,121 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.042480 4.871582 cm +0.000000 0.000000 0.000000 scn +1.003418 0.000000 m +1.896973 0.000000 l +2.468262 0.000000 2.900391 0.439453 2.900391 0.996094 c +2.900391 2.299805 l +4.541016 2.189941 6.979980 2.116699 8.957520 2.116699 c +10.935059 2.116699 13.366699 2.189941 15.007324 2.299805 c +15.007324 0.996094 l +15.007324 0.439453 15.446777 0.000000 16.010742 0.000000 c +16.904297 0.000000 l +17.475586 0.000000 17.907715 0.439453 17.907715 0.996094 c +17.907715 5.427246 l +17.907715 6.745605 17.680664 7.485352 16.984863 8.378906 c +16.318359 9.206543 l +16.047363 10.524902 15.563965 11.901855 15.307617 12.451172 c +14.890137 13.337402 14.077148 13.872070 13.044434 14.011230 c +12.487793 14.084473 10.942383 14.128418 8.957520 14.128418 c +6.972656 14.128418 5.419922 14.077148 4.870605 14.011230 c +3.830566 13.879395 3.017578 13.337402 2.600098 12.451172 c +2.343750 11.901855 1.860352 10.524902 1.589355 9.206543 c +0.922852 8.378906 l +0.227051 7.485352 0.000000 6.745605 0.000000 5.427246 c +0.000000 0.996094 l +0.000000 0.439453 0.439453 0.000000 1.003418 0.000000 c +h +3.339844 9.916992 m +3.493652 10.605469 3.801270 11.520996 3.999023 11.887207 c +4.226074 12.304688 4.482422 12.465820 4.973145 12.531738 c +5.471191 12.604980 6.730957 12.641602 8.957520 12.641602 c +11.176758 12.641602 12.436523 12.612305 12.941895 12.531738 c +13.425293 12.465820 13.681641 12.304688 13.908691 11.887207 c +14.113770 11.520996 14.406738 10.605469 14.575195 9.916992 c +14.655762 9.572754 14.509277 9.404297 14.143066 9.418945 c +13.073730 9.477539 11.667480 9.550781 8.957520 9.550781 c +6.247559 9.550781 4.833984 9.477539 3.764648 9.418945 c +3.398438 9.404297 3.259277 9.572754 3.339844 9.916992 c +h +3.537598 4.196777 m +2.819824 4.196777 2.277832 4.738770 2.277832 5.456543 c +2.277832 6.174316 2.819824 6.716309 3.537598 6.716309 c +4.255371 6.716309 4.797363 6.174316 4.797363 5.456543 c +4.797363 4.738770 4.255371 4.196777 3.537598 4.196777 c +h +14.370117 4.196777 m +13.659668 4.196777 13.117676 4.738770 13.117676 5.456543 c +13.117676 6.174316 13.659668 6.716309 14.370117 6.716309 c +15.087891 6.716309 15.629883 6.174316 15.629883 5.456543 c +15.629883 4.738770 15.087891 4.196777 14.370117 4.196777 c +h +6.950684 4.519043 m +6.416016 4.519043 6.035156 4.892578 6.035156 5.427246 c +6.035156 5.961914 6.416016 6.342773 6.950684 6.342773 c +10.964355 6.342773 l +11.499023 6.342773 11.872559 5.961914 11.872559 5.427246 c +11.872559 4.892578 11.499023 4.519043 10.964355 4.519043 c +6.950684 4.519043 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2543 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002633 00000 n +0000002656 00000 n +0000002829 00000 n +0000002903 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2962 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json index 91af4d8829..f4ae46288f 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "transit.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "train_24.pdf", + "idiom" : "universal" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/train_24.pdf b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/train_24.pdf new file mode 100644 index 0000000000..a38aba3309 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/train_24.pdf @@ -0,0 +1,129 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.305664 2.266602 cm +0.000000 0.000000 0.000000 scn +1.259766 0.000000 m +1.750488 0.000000 l +2.006836 0.000000 2.153320 0.080566 2.285156 0.300293 c +2.695312 0.952148 l +10.620117 0.952148 l +11.030273 0.300293 l +11.162109 0.080566 11.308594 0.000000 11.564941 0.000000 c +11.997070 0.000000 l +12.436523 0.000000 12.648926 0.439453 12.414551 0.820312 c +10.876465 3.303223 l +10.891113 3.303223 l +12.465820 3.303223 13.381348 4.321289 13.381348 5.859375 c +13.381348 11.359863 l +13.381348 13.388672 13.176270 14.934082 12.912598 16.091309 c +12.539062 17.819824 11.586914 18.845215 9.880371 19.064941 c +9.294434 19.145508 8.107910 19.233398 6.687012 19.233398 c +5.266113 19.233398 4.086914 19.145508 3.500977 19.064941 c +1.794434 18.845215 0.834961 17.819824 0.468750 16.091309 c +0.205078 14.934082 0.000000 13.388672 0.000000 11.359863 c +0.000000 5.859375 l +0.000000 4.343262 0.893555 3.325195 2.438965 3.303223 c +0.864258 0.783691 l +0.644531 0.432129 0.827637 0.000000 1.259766 0.000000 c +h +4.768066 16.530762 m +4.438477 16.530762 4.211426 16.765137 4.211426 17.094727 c +4.211426 17.424316 4.438477 17.651367 4.768066 17.651367 c +8.613281 17.651367 l +8.942871 17.651367 9.169922 17.424316 9.169922 17.094727 c +9.169922 16.765137 8.942871 16.530762 8.613281 16.530762 c +4.768066 16.530762 l +h +6.687012 8.393555 m +4.899902 8.400879 3.398438 8.474121 2.607422 8.569336 c +2.124023 8.627930 1.896973 8.957520 1.896973 9.418945 c +1.896973 11.250000 l +1.896973 12.707520 2.058105 13.967285 2.160645 14.450684 c +2.233887 14.824219 2.475586 15.095215 2.849121 15.139160 c +3.669434 15.241699 4.760742 15.292969 6.687012 15.307617 c +8.605957 15.307617 9.704590 15.241699 10.517578 15.139160 c +10.891113 15.095215 11.140137 14.824219 11.220703 14.450684 c +11.315918 13.967285 11.484375 12.707520 11.484375 11.250000 c +11.484375 9.418945 l +11.484375 8.950195 11.257324 8.627930 10.766602 8.569336 c +9.704590 8.452148 8.188477 8.393555 6.687012 8.393555 c +h +10.224609 5.024414 m +9.631348 5.024414 9.177246 5.471191 9.177246 6.071777 c +9.177246 6.665039 9.631348 7.111816 10.224609 7.111816 c +10.817871 7.111816 11.264648 6.665039 11.264648 6.071777 c +11.264648 5.471191 10.803223 5.024414 10.224609 5.024414 c +h +3.149414 5.031738 m +2.548828 5.031738 2.116699 5.471191 2.116699 6.071777 c +2.116699 6.665039 2.556152 7.111816 3.149414 7.111816 c +3.750000 7.111816 4.189453 6.665039 4.189453 6.071777 c +4.189453 5.471191 3.742676 5.031738 3.149414 5.031738 c +h +3.500977 2.263184 m +4.152832 3.303223 l +9.162598 3.303223 l +9.814453 2.263184 l +3.500977 2.263184 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2613 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002703 00000 n +0000002726 00000 n +0000002899 00000 n +0000002973 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3032 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png b/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png deleted file mode 100644 index 73c9cf47c9..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Location/DirectionsTransit.imageset/transit.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json index d07f928364..58b905e636 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "walking.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "walk_24.pdf", + "idiom" : "universal" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walk_24.pdf b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walk_24.pdf new file mode 100644 index 0000000000..cfcabb2bd7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walk_24.pdf @@ -0,0 +1,118 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 7.000000 3.275391 cm +0.000000 0.000000 0.000000 scn +5.515137 14.157715 m +6.503906 14.157715 7.302246 14.978027 7.302246 15.930176 c +7.302246 16.918945 6.503906 17.724609 5.515137 17.724609 c +4.526367 17.724609 3.742676 16.918945 3.742676 15.930176 c +3.742676 14.978027 4.526367 14.157715 5.515137 14.157715 c +h +8.034668 0.000000 m +8.474121 0.000000 8.869629 0.322266 8.869629 0.842285 c +8.869629 1.032715 8.811035 1.208496 8.708496 1.413574 c +7.104492 4.665527 l +6.958008 4.965820 6.796875 5.222168 6.650391 5.419922 c +5.690918 6.789551 l +5.749512 6.965332 l +6.020508 7.829590 6.115723 8.349609 6.174316 9.162598 c +6.342773 11.433105 l +6.423340 12.568359 5.764160 13.505859 4.562988 13.505859 c +3.698730 13.505859 3.046875 13.044434 2.248535 12.253418 c +1.025391 11.052246 l +0.607910 10.634766 0.476074 10.305176 0.439453 9.785156 c +0.285645 7.917480 l +0.241699 7.397461 0.541992 7.053223 1.003418 7.023926 c +1.457520 6.994629 1.787109 7.258301 1.838379 7.814941 c +2.006836 9.858398 l +2.724609 10.495605 l +2.863770 10.612793 3.054199 10.546875 3.039551 10.349121 c +2.907715 8.503418 l +2.834473 7.602539 3.244629 6.958008 3.684082 6.401367 c +5.273438 4.401855 l +5.427246 4.211426 5.449219 4.138184 5.522461 3.991699 c +7.221680 0.541992 l +7.419434 0.146484 7.719727 0.000000 8.034668 0.000000 c +h +8.408203 8.774414 m +10.231934 8.774414 l +10.759277 8.774414 11.081543 9.082031 11.081543 9.543457 c +11.088867 10.012207 10.759277 10.327148 10.224609 10.327148 c +8.327637 10.327148 l +7.016602 11.733398 l +6.877441 9.667969 l +7.426758 9.118652 l +7.697754 8.847656 7.946777 8.774414 8.408203 8.774414 c +h +0.820312 0.000000 m +1.083984 0.000000 1.325684 0.117188 1.494141 0.329590 c +3.881836 3.178711 l +4.130859 3.471680 4.174805 3.559570 4.270020 3.845215 c +4.328613 4.020996 4.372559 4.189453 4.416504 4.357910 c +3.024902 6.093750 l +2.629395 4.255371 l +0.336914 1.545410 l +0.131836 1.303711 0.000000 1.098633 0.000000 0.812988 c +0.000000 0.344238 0.366211 0.000000 0.820312 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 2052 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002142 00000 n +0000002165 00000 n +0000002338 00000 n +0000002412 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2471 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png b/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png deleted file mode 100644 index 0376c5ddc8..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Location/DirectionsWalking.imageset/walking.png and /dev/null differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 93e0ba7cec..85b5c919af 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4801,60 +4801,75 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if canManageInvitations, let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0, strongSelf.inviteRequestsContext == nil { - let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil)) - strongSelf.inviteRequestsContext = inviteRequestsContext - - strongSelf.inviteRequestsDisposable.set((inviteRequestsContext.state - |> deliverOnMainQueue).start(next: { [weak self] requestsState in - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in - return state - .updatedTitlePanelContext({ context in - if requestsState.count > 0 { + if canManageInvitations, let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0 { + if strongSelf.inviteRequestsContext == nil { + let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil)) + strongSelf.inviteRequestsContext = inviteRequestsContext + + strongSelf.inviteRequestsDisposable.set((combineLatest(queue: Queue.mainQueue(), inviteRequestsContext.state, ApplicationSpecificNotice.dismissedInvitationRequests(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId))).start(next: { [weak self] requestsState, dismissedInvitationRequests in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in + return state + .updatedTitlePanelContext({ context in let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3)) - if !context.contains(where: { - switch $0 { - case .inviteRequests(peers, requestsState.count): - return true - default: - return false - } - }) { - var updatedContexts = context.filter { c in - if case .inviteRequests = c { - return false - } else { - return true + + var peersDismissed = false + if let dismissedInvitationRequests = dismissedInvitationRequests, Set(peers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) { + peersDismissed = true + } + + if requestsState.count > 0 && !peersDismissed { + if !context.contains(where: { + switch $0 { + case .inviteRequests(peers, requestsState.count): + return true + default: + return false } + }) { + var updatedContexts = context.filter { c in + if case .inviteRequests = c { + return false + } else { + return true + } + } + updatedContexts.append(.inviteRequests(peers, requestsState.count)) + return updatedContexts.sorted() + } else { + return context } - updatedContexts.append(.inviteRequests(peers, requestsState.count)) - return updatedContexts.sorted() } else { - return context - } - } else { - if let index = context.firstIndex(where: { - switch $0 { - case .inviteRequests: - return true - default: - return false + if let index = context.firstIndex(where: { + switch $0 { + case .inviteRequests: + return true + default: + return false + } + }) { + var updatedContexts = context + updatedContexts.remove(at: index) + return updatedContexts + } else { + return context } - }) { - var updatedContexts = context - updatedContexts.remove(at: index) - return updatedContexts - } else { - return context } - } + }) + .updatedSlowmodeState(slowmodeState) }) - .updatedSlowmodeState(slowmodeState) + })) + } else if let inviteRequestsContext = strongSelf.inviteRequestsContext { + let _ = (inviteRequestsContext.state + |> take(1) + |> deliverOnMainQueue).start(next: { [weak inviteRequestsContext] state in + if state.count != inviteRequestsPending { + inviteRequestsContext?.loadMore() + } }) - })) + } } if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo { @@ -5094,7 +5109,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: transition.peerType, networkType: transition.networkType, animateIn: false, reason: transition.reason, flashIndicators: transition.flashIndicators), updateSizeAndInsets) - }, updateExtraNavigationBarBackgroundHeight: { value in + }, updateExtraNavigationBarBackgroundHeight: { value, _ in strongSelf.additionalNavigationBarBackgroundHeight = value }) @@ -8247,9 +8262,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var navigationBarTransition = transition self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) - }, updateExtraNavigationBarBackgroundHeight: { value in + }, updateExtraNavigationBarBackgroundHeight: { value, extraNavigationTransition in + navigationBarTransition = extraNavigationTransition self.additionalNavigationBarBackgroundHeight = value }) @@ -8269,7 +8286,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.suspendNavigationBarLayout = false if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { self.suspendedNavigationBarLayout = suspendedNavigationBarLayout - self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: navigationBarTransition) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4ae7c5aa92..492a7e02f4 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -649,7 +649,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputMediaNode?.simulateUpdateLayout(isVisible: isInFocus) } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat) -> Void) { + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, ContainedViewLayoutTransition) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { transition = .immediate @@ -797,17 +797,26 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = false var titleAccessoryPanelHeight: CGFloat? var titleAccessoryPanelBackgroundHeight: CGFloat? + var extraTransition = transition if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.titleAccessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { if self.titleAccessoryPanelNode != titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = self.titleAccessoryPanelNode self.titleAccessoryPanelNode = titleAccessoryPanelNode immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = true self.titleAccessoryPanelContainer.addSubnode(titleAccessoryPanelNode) + + titleAccessoryPanelNode.clipsToBounds = true + extraTransition = .animated(duration: 0.2, curve: .easeInOut) } let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) titleAccessoryPanelHeight = layoutResult.insetHeight titleAccessoryPanelBackgroundHeight = layoutResult.backgroundHeight + if immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance { + titleAccessoryPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + titleAccessoryPanelNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -layoutResult.backgroundHeight, 0.0) + extraTransition.updateSublayerTransformOffset(layer: titleAccessoryPanelNode.layer, offset: CGPoint()) + } } else if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = titleAccessoryPanelNode self.titleAccessoryPanelNode = nil @@ -1009,7 +1018,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { insets.top += panelHeight } - updateExtraNavigationBarBackgroundHeight(titleAccessoryPanelBackgroundHeight ?? 0.0) + updateExtraNavigationBarBackgroundHeight(titleAccessoryPanelBackgroundHeight ?? 0.0, extraTransition) var importStatusPanelFrame: CGRect? if let _ = self.chatImportStatusPanel, let panelHeight = importStatusPanelHeight { @@ -2241,7 +2250,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) - }, updateExtraNavigationBarBackgroundHeight: { _ in + }, updateExtraNavigationBarBackgroundHeight: { _, _ in }) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index b51eb290d0..30fcdd0d04 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -120,14 +120,16 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat return panel } case let .inviteRequests(peers, count): - if let currentPanel = currentPanel as? ChatInviteRequestsTitlePanelNode { - currentPanel.update(peers: peers, count: count) - return currentPanel - } else { - let panel = ChatInviteRequestsTitlePanelNode(context: context) - panel.interfaceInteraction = interfaceInteraction - panel.update(peers: peers, count: count) - return panel + if let peerId = chatPresentationInterfaceState.renderedPeer?.peerId { + if let currentPanel = currentPanel as? ChatInviteRequestsTitlePanelNode { + currentPanel.update(peerId: peerId, peers: peers, count: count) + return currentPanel + } else { + let panel = ChatInviteRequestsTitlePanelNode(context: context) + panel.interfaceInteraction = interfaceInteraction + panel.update(peerId: peerId, peers: peers, count: count) + return panel + } } } } diff --git a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift index c1757f8ed4..190f43aff5 100644 --- a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift @@ -7,6 +7,7 @@ import TelegramCore import TelegramPresentationData import LocalizedPeerData import TelegramStringFormatting +import TelegramNotices import AnimatedAvatarSetNode import AccountContext @@ -119,6 +120,10 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { private var theme: PresentationTheme? + private var peerId: PeerId? + private var peers: [EnginePeer] = [] + private var count: Int32 = 0 + init(context: AccountContext) { self.context = context @@ -142,9 +147,11 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { self.addSubnode(self.avatarsNode) } - private var requestsCount: Int32 = 0 - func update(peers: [EnginePeer], count: Int32) { - self.requestsCount = count + + func update(peerId: PeerId, peers: [EnginePeer], count: Int32) { + self.peerId = peerId + self.peers = peers + self.count = count let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.avatarsContent = self.avatarsContext.update(peers: peers, animated: false) @@ -176,7 +183,7 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { self.button = view } - self.button?.setTitle(interfaceState.strings.Conversation_RequestsToJoin(self.requestsCount), for: []) + self.button?.setTitle(interfaceState.strings.Conversation_RequestsToJoin(self.count), for: []) let maxInset = max(contentRightInset, leftInset) let buttonWidth = floor(width - maxInset * 2.0) @@ -198,7 +205,12 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { } @objc func closePressed() { -// self.interfaceInteraction?.dismissReportPeer() + guard let peerId = self.peerId else { + return + } + + let ids = peers.map { $0.id.toInt64() } + let _ = ApplicationSpecificNotice.setDismissedInvitationRequests(accountManager: context.sharedContext.accountManager, peerId: peerId, values: ids).start() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index aaf9f53ee6..d4dd82733a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -283,6 +283,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } else if let type = webpage.type { switch type { + case "telegram_channel_request": + actionTitle = item.presentationData.strings.Conversation_RequestToJoinChannel + case "telegram_chat_request", "telegram_megagroup_request": + actionTitle = item.presentationData.strings.Conversation_RequestToJoinGroup case "telegram_channel": actionTitle = item.presentationData.strings.Conversation_ViewChannel case "telegram_chat", "telegram_megagroup": diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index d4d72c4192..a0c820f7c6 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -153,7 +153,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: new, stickerPacks: [new], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) return true } - case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite): + case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite), let .participantJoinByRequest(invite, _): if !invite.link.hasSuffix("...") { if invite.isPermanent { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) @@ -248,7 +248,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } return false }, openPeer: { [weak self] peerId, _, message in - if let peerId = peerId { + if let peerId = peerId, peerId != context.account.peerId { self?.openPeer(peerId: peerId, peer: message?.peers[peerId]) } }, openPeerMention: { [weak self] name in diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 71a2658982..f7893b8466 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -1404,6 +1404,39 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes())) + case let .participantJoinByRequest(invite, approvedBy): + var peers = SimpleDictionary() + var author: Peer? + var approver: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + if let peer = self.entry.peers[approvedBy] { + approver = peer + peers[approvedBy] = approver + } + + var text: String = "" + var entities: [MessageTextEntity] = [] + + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_JoinedViaRequest(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link.replacingOccurrences(of: "https://", with: ""), approver.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "") + + appendAttributedText(text: rawText, generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } else if index == 1 { + return [.Bold] + } else if index == 2, let approver = approver { + return [.TextMention(peerId: approver.id)] + } + return [] + }, to: &text, entities: &entities) + + let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes())) }