Various Improvements

This commit is contained in:
Ilya Laktyushin 2021-10-14 04:40:07 +04:00
parent 1ec1f5ca78
commit d8d344d2c1
40 changed files with 1932 additions and 293 deletions

View File

@ -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$@";

View File

@ -55,6 +55,7 @@ swift_library(
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/AvatarNode:AvatarNode",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
],
visibility = [
"//visibility:public",

View File

@ -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 .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)

View File

@ -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<PresentationData, NoError>)? = 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<Bool>(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<Bool, NoError>
init(sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal<Bool, NoError>) {
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)
}
}

View File

@ -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)

View File

@ -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<Bool> = 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<Bool, NoError> 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<String?>()
private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
private var _hasDim: Bool = false
override public var hasDim: Bool {
return _hasDim
}
private var processedPeerIdsPromise = ValuePromise<Set<PeerId>>(Set())
private var processedPeerIds = Set<PeerId>() {
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<PeerInvitationImportersContext?>(value: nil)
let processedPeerIds = self.processedPeerIdsPromise
let searchQuery = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> 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
}
}

View File

@ -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
@ -131,23 +135,29 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
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 {
@ -232,31 +253,239 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
}
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)
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 = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
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
}
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
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))

View File

@ -66,7 +66,7 @@ public final class JoinLinkPreviewController: ViewController {
}
self.displayNodeDidLoad()
let signal: Signal<ExternalJoiningChatState, NoError>
let signal: Signal<ExternalJoiningChatState, JoinLinkInfoError>
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())
}

View File

@ -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<Void, NoError>) {
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

View File

@ -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() {
}
}

View File

@ -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<Void, NoError>?, (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
}
directionsButtonNode.subtitle = item.eta
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)
}
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))

View File

@ -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)

View File

@ -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<Double?, NoError> = .single(nil)
var eta: Signal<(Double?, Double?, Double?)?, NoError> = .single(nil)
var address: Signal<String?, NoError> = .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
}

View File

@ -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<Bool?>(value: nil)
let previousInvitation = Atomic<ExportedInvitation?>(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<PeerInvitationImportersState?>(nil)
let importersContext: Signal<PeerInvitationImportersContext?, NoError> = 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)
}
}

View File

@ -6,7 +6,6 @@ import Postbox
import TelegramPresentationData
import ActivityIndicator
import RadialStatusNode
import RadialStatusNode
public enum ShareLoadingState {
case preparing

View File

@ -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 {

View File

@ -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) }

View File

@ -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 {

View File

@ -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
}

View File

@ -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 {

View File

@ -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))

View File

@ -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<ExternalJoiningChatState, NoError> {
return account.network.request(Api.functions.messages.checkChatInvite(hash: hash))
func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal<ExternalJoiningChatState, JoinLinkInfoError> {
return account.network.request(Api.functions.messages.checkChatInvite(hash: hash), automaticFloodWait: false)
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.ChatInvite?, NoError> in
|> `catch` { error -> Signal<Api.ChatInvite?, JoinLinkInfoError> in
if error.errorDescription.hasPrefix("FLOOD_WAIT") {
return .fail(.flood)
} else {
return .single(nil)
}
|> mapToSignal { (result) -> Signal<ExternalJoiningChatState, NoError> in
}
|> mapToSignal { result -> Signal<ExternalJoiningChatState, JoinLinkInfoError> 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<E
return .alreadyJoined(peer.id)
})
|> castError(JoinLinkInfoError.self)
}
return .single(.invalidHash)
case let .chatInvitePeek(chat, expires):
@ -107,6 +117,7 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal<E
return .peek(peer.id, expires)
})
|> castError(JoinLinkInfoError.self)
}
return .single(.invalidHash)
}

View File

@ -486,7 +486,7 @@ public extension TelegramEngine {
return _internal_joinChatInteractively(with: hash, account: self.account)
}
public func joinLinkInformation(_ hash: String) -> Signal<ExternalJoiningChatState, NoError> {
public func joinLinkInformation(_ hash: String) -> Signal<ExternalJoiningChatState, JoinLinkInfoError> {
return _internal_joinLinkInformation(hash, account: self.account)
}

View File

@ -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<TelegramAccountManagerTypes>, 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<TelegramAccountManagerTypes>, peerId: PeerId, values: [Int64]) -> Signal<Void, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
}

View File

@ -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" : {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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

View File

@ -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" : {

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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" : {

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -4801,20 +4801,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
if canManageInvitations, let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0, strongSelf.inviteRequestsContext == nil {
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((inviteRequestsContext.state
|> deliverOnMainQueue).start(next: { [weak self] requestsState in
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: false, interactive: false, { state in
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
return state
.updatedTitlePanelContext({ context in
if requestsState.count > 0 {
let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3))
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):
@ -4855,6 +4861,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
.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)
}
}

View File

@ -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
})
}
}

View File

@ -120,17 +120,19 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
return panel
}
case let .inviteRequests(peers, count):
if let peerId = chatPresentationInterfaceState.renderedPeer?.peerId {
if let currentPanel = currentPanel as? ChatInviteRequestsTitlePanelNode {
currentPanel.update(peers: peers, count: count)
currentPanel.update(peerId: peerId, peers: peers, count: count)
return currentPanel
} else {
let panel = ChatInviteRequestsTitlePanelNode(context: context)
panel.interfaceInteraction = interfaceInteraction
panel.update(peers: peers, count: count)
panel.update(peerId: peerId, peers: peers, count: count)
return panel
}
}
}
}
return nil
}

View File

@ -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? {

View File

@ -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":

View File

@ -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

View File

@ -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<PeerId, Peer>()
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()))
}