mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Folders
This commit is contained in:
parent
cd70df2448
commit
c0dbb68a3c
@ -298,6 +298,7 @@ public enum ResolvedUrl {
|
|||||||
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
|
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
|
||||||
case invoice(slug: String, invoice: TelegramMediaInvoice?)
|
case invoice(slug: String, invoice: TelegramMediaInvoice?)
|
||||||
case premiumOffer(reference: String?)
|
case premiumOffer(reference: String?)
|
||||||
|
case chatFolder(slug: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NavigateToChatKeepStack {
|
public enum NavigateToChatKeepStack {
|
||||||
|
@ -43,6 +43,7 @@ import PeerInfoUI
|
|||||||
import ComponentDisplayAdapters
|
import ComponentDisplayAdapters
|
||||||
import ChatListHeaderComponent
|
import ChatListHeaderComponent
|
||||||
import ChatListTitleView
|
import ChatListTitleView
|
||||||
|
import InviteLinksUI
|
||||||
|
|
||||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||||
if listNode.scroller.isDragging {
|
if listNode.scroller.isDragging {
|
||||||
@ -1539,6 +1540,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
})
|
})
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
|
||||||
|
for filter in filters {
|
||||||
|
if filter.id == filterId, case let .filter(_, _, _, data) = filter {
|
||||||
|
if !data.includePeers.peers.isEmpty {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { c, f in
|
||||||
|
c.dismiss(completion: {
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.shareFolder(filterId: filterId, data: data)
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2676,6 +2699,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func shareFolder(filterId: Int32, data: ChatListFilterData) {
|
||||||
|
self.push(folderInviteLinkListController(context: self.context, filterId: filterId, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { _ in
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
private func askForFilterRemoval(id: Int32) {
|
private func askForFilterRemoval(id: Int32) {
|
||||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ private enum ChatListFilterPresetEntrySortId: Comparable {
|
|||||||
case let .inviteLink(rhsIndex):
|
case let .inviteLink(rhsIndex):
|
||||||
return lhsIndex < rhsIndex
|
return lhsIndex < rhsIndex
|
||||||
case .inviteLinkFooter:
|
case .inviteLinkFooter:
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
case .inviteLinkFooter:
|
case .inviteLinkFooter:
|
||||||
switch rhs {
|
switch rhs {
|
||||||
@ -1059,8 +1059,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
|||||||
|
|
||||||
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
|
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
|
||||||
if let currentPreset {
|
if let currentPreset {
|
||||||
sharedLinks.set(context.engine.peers.getExportedChatLinks(id: currentPreset.id)
|
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id)
|
||||||
|> map(Optional.init))
|
|> map(Optional.init)))
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
|
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
|
||||||
@ -1266,20 +1266,43 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
|||||||
},
|
},
|
||||||
createLink: {
|
createLink: {
|
||||||
if let currentPreset, let data = currentPreset.data, !data.includePeers.peers.isEmpty {
|
if let currentPreset, let data = currentPreset.data, !data.includePeers.peers.isEmpty {
|
||||||
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: nil))
|
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { updatedLink in
|
||||||
|
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
|
||||||
|
guard var links else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*if data.isShared {
|
if let updatedLink {
|
||||||
|
links.insert(updatedLink, at: 0)
|
||||||
} else {
|
sharedLinks.set(.single(links))
|
||||||
let _ = (context.engine.peers.exportChatFolder(filterId: currentPreset.id, title: "Link", peerIds: data.includePeers.peers)
|
}
|
||||||
|> deliverOnMainQueue).start(completed: {
|
|
||||||
dismissImpl?()
|
|
||||||
})
|
})
|
||||||
}*/
|
}))
|
||||||
}
|
}
|
||||||
}, openLink: { link in
|
}, openLink: { link in
|
||||||
if let currentPreset, let data = currentPreset.data {
|
if let currentPreset, let data = currentPreset.data {
|
||||||
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: link))
|
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: link, linkUpdated: { updatedLink in
|
||||||
|
if updatedLink != link {
|
||||||
|
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
|
||||||
|
guard var links else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let updatedLink {
|
||||||
|
if let index = links.firstIndex(where: { $0 == link }) {
|
||||||
|
links.remove(at: index)
|
||||||
|
}
|
||||||
|
links.insert(updatedLink, at: 0)
|
||||||
|
sharedLinks.set(.single(links))
|
||||||
|
} else {
|
||||||
|
if let index = links.firstIndex(where: { $0 == link }) {
|
||||||
|
links.remove(at: index)
|
||||||
|
sharedLinks.set(.single(links))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -6,16 +6,17 @@ import LegacyComponents
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
public struct CheckNodeTheme {
|
public struct CheckNodeTheme {
|
||||||
public let backgroundColor: UIColor
|
public var backgroundColor: UIColor
|
||||||
public let strokeColor: UIColor
|
public var strokeColor: UIColor
|
||||||
public let borderColor: UIColor
|
public var borderColor: UIColor
|
||||||
public let overlayBorder: Bool
|
public var overlayBorder: Bool
|
||||||
public let hasInset: Bool
|
public var hasInset: Bool
|
||||||
public let hasShadow: Bool
|
public var hasShadow: Bool
|
||||||
public let filledBorder: Bool
|
public var filledBorder: Bool
|
||||||
public let borderWidth: CGFloat?
|
public var borderWidth: CGFloat?
|
||||||
|
public var isDottedBorder: Bool
|
||||||
|
|
||||||
public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) {
|
public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil, isDottedBorder: Bool = false) {
|
||||||
self.backgroundColor = backgroundColor
|
self.backgroundColor = backgroundColor
|
||||||
self.strokeColor = strokeColor
|
self.strokeColor = strokeColor
|
||||||
self.borderColor = borderColor
|
self.borderColor = borderColor
|
||||||
@ -24,6 +25,7 @@ public struct CheckNodeTheme {
|
|||||||
self.hasShadow = hasShadow
|
self.hasShadow = hasShadow
|
||||||
self.filledBorder = filledBorder
|
self.filledBorder = filledBorder
|
||||||
self.borderWidth = borderWidth
|
self.borderWidth = borderWidth
|
||||||
|
self.isDottedBorder = isDottedBorder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +170,9 @@ public class CheckNode: ASDisplayNode {
|
|||||||
let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35)
|
let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35)
|
||||||
|
|
||||||
context.setStrokeColor(parameters.theme.borderColor.cgColor)
|
context.setStrokeColor(parameters.theme.borderColor.cgColor)
|
||||||
|
if parameters.theme.isDottedBorder {
|
||||||
|
context.setLineDash(phase: 0.0, lengths: [4.0, 4.0])
|
||||||
|
}
|
||||||
context.setLineWidth(borderWidth)
|
context.setLineWidth(borderWidth)
|
||||||
|
|
||||||
let maybeScaleOut = {
|
let maybeScaleOut = {
|
||||||
|
@ -27,8 +27,8 @@ private final class FolderInviteLinkListControllerArguments {
|
|||||||
let shareMainLink: (String) -> Void
|
let shareMainLink: (String) -> Void
|
||||||
let openMainLink: (String) -> Void
|
let openMainLink: (String) -> Void
|
||||||
let copyLink: (String) -> Void
|
let copyLink: (String) -> Void
|
||||||
let mainLinkContextAction: (String?, ASDisplayNode, ContextGesture?) -> Void
|
let mainLinkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void
|
||||||
let peerAction: (EnginePeer) -> Void
|
let peerAction: (EnginePeer, Bool) -> Void
|
||||||
let generateLink: () -> Void
|
let generateLink: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -36,8 +36,8 @@ private final class FolderInviteLinkListControllerArguments {
|
|||||||
shareMainLink: @escaping (String) -> Void,
|
shareMainLink: @escaping (String) -> Void,
|
||||||
openMainLink: @escaping (String) -> Void,
|
openMainLink: @escaping (String) -> Void,
|
||||||
copyLink: @escaping (String) -> Void,
|
copyLink: @escaping (String) -> Void,
|
||||||
mainLinkContextAction: @escaping (String?, ASDisplayNode, ContextGesture?) -> Void,
|
mainLinkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void,
|
||||||
peerAction: @escaping (EnginePeer) -> Void,
|
peerAction: @escaping (EnginePeer, Bool) -> Void,
|
||||||
generateLink: @escaping () -> Void
|
generateLink: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -68,7 +68,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool)
|
case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool)
|
||||||
|
|
||||||
case peersHeader(String)
|
case peersHeader(String)
|
||||||
case peer(index: Int, peer: EnginePeer, isSelected: Bool)
|
case peer(index: Int, peer: EnginePeer, isSelected: Bool, isEnabled: Bool)
|
||||||
case peersInfo(String)
|
case peersInfo(String)
|
||||||
|
|
||||||
var section: ItemListSectionId {
|
var section: ItemListSectionId {
|
||||||
@ -94,7 +94,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
return .index(4)
|
return .index(4)
|
||||||
case .peersInfo:
|
case .peersInfo:
|
||||||
return .index(5)
|
return .index(5)
|
||||||
case let .peer(_, peer, _):
|
case let .peer(_, peer, _, _):
|
||||||
return .peer(peer.id)
|
return .peer(peer.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
return 2
|
return 2
|
||||||
case .peersHeader:
|
case .peersHeader:
|
||||||
return 4
|
return 4
|
||||||
case let .peer(index, _, _):
|
case let .peer(index, _, _, _):
|
||||||
return 10 + index
|
return 10 + index
|
||||||
case .peersInfo:
|
case .peersInfo:
|
||||||
return 1000
|
return 1000
|
||||||
@ -148,8 +148,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .peer(index, peer, isSelected):
|
case let .peer(index, peer, isSelected, isEnabled):
|
||||||
if case .peer(index, peer, isSelected) = rhs {
|
if case .peer(index, peer, isSelected, isEnabled) = rhs {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -180,7 +180,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
arguments.generateLink()
|
arguments.generateLink()
|
||||||
}
|
}
|
||||||
}, contextAction: { node, gesture in
|
}, contextAction: { node, gesture in
|
||||||
arguments.mainLinkContextAction(link?.link, node, gesture)
|
arguments.mainLinkContextAction(link, node, gesture)
|
||||||
}, viewAction: {
|
}, viewAction: {
|
||||||
if let link {
|
if let link {
|
||||||
arguments.openMainLink(link.link)
|
arguments.openMainLink(link.link)
|
||||||
@ -190,7 +190,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .peersInfo(text):
|
case let .peersInfo(text):
|
||||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
|
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
|
||||||
case let .peer(_, peer, isSelected):
|
case let .peer(_, peer, isSelected, isEnabled):
|
||||||
|
//TODO:localize
|
||||||
return ItemListPeerItem(
|
return ItemListPeerItem(
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
dateTimeFormat: PresentationDateTimeFormat(),
|
dateTimeFormat: PresentationDateTimeFormat(),
|
||||||
@ -198,15 +199,15 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
context: arguments.context,
|
context: arguments.context,
|
||||||
peer: peer,
|
peer: peer,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
text: .none,
|
text: .text(isEnabled ? "you can invite others here" : "you can't invite others here", .secondary),
|
||||||
label: .none,
|
label: .none,
|
||||||
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
|
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||||
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck),
|
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
sectionId: self.section,
|
sectionId: self.section,
|
||||||
action: {
|
action: {
|
||||||
arguments.peerAction(peer)
|
arguments.peerAction(peer, isEnabled)
|
||||||
},
|
},
|
||||||
setPeerIdWithRevealedOptions: { _, _ in
|
setPeerIdWithRevealedOptions: { _, _ in
|
||||||
},
|
},
|
||||||
@ -217,6 +218,21 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canShareLinkToPeer(peer: EnginePeer) -> Bool {
|
||||||
|
var isEnabled = false
|
||||||
|
switch peer {
|
||||||
|
case let .channel(channel):
|
||||||
|
if channel.hasPermission(.inviteMembers) {
|
||||||
|
isEnabled = true
|
||||||
|
} else if channel.username != nil {
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
private func folderInviteLinkListControllerEntries(
|
private func folderInviteLinkListControllerEntries(
|
||||||
presentationData: PresentationData,
|
presentationData: PresentationData,
|
||||||
state: FolderInviteLinkListControllerState,
|
state: FolderInviteLinkListControllerState,
|
||||||
@ -224,16 +240,38 @@ private func folderInviteLinkListControllerEntries(
|
|||||||
) -> [InviteLinksListEntry] {
|
) -> [InviteLinksListEntry] {
|
||||||
var entries: [InviteLinksListEntry] = []
|
var entries: [InviteLinksListEntry] = []
|
||||||
|
|
||||||
entries.append(.header("Anyone with this link can add Gaming Club folder and the 2 chats selected below."))
|
let chatCountString: String
|
||||||
|
let peersHeaderString: String
|
||||||
|
if state.selectedPeerIds.isEmpty {
|
||||||
|
chatCountString = "Anyone with this link can add Gaming Club folder and the chats selected below."
|
||||||
|
peersHeaderString = "CHATS"
|
||||||
|
} else if state.selectedPeerIds.count == 1 {
|
||||||
|
chatCountString = "Anyone with this link can add Gaming Club folder and the 1 chat selected below."
|
||||||
|
peersHeaderString = "1 CHAT SELECTED"
|
||||||
|
} else {
|
||||||
|
chatCountString = "Anyone with this link can add Gaming Club folder and the \(state.selectedPeerIds.count) chats selected below."
|
||||||
|
peersHeaderString = "\(state.selectedPeerIds.count) CHATS SELECTED"
|
||||||
|
}
|
||||||
|
entries.append(.header(chatCountString))
|
||||||
|
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
|
|
||||||
entries.append(.mainLinkHeader("INVITE LINK"))
|
entries.append(.mainLinkHeader("INVITE LINK"))
|
||||||
entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink))
|
entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink))
|
||||||
|
|
||||||
entries.append(.peersHeader("\(allPeers.count) CHATS SELECTED"))
|
entries.append(.peersHeader(peersHeaderString))
|
||||||
for peer in allPeers {
|
|
||||||
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id)))
|
var sortedPeers: [EnginePeer] = []
|
||||||
|
for peer in allPeers.filter({ canShareLinkToPeer(peer: $0) }) {
|
||||||
|
sortedPeers.append(peer)
|
||||||
|
}
|
||||||
|
for peer in allPeers.filter({ !canShareLinkToPeer(peer: $0) }) {
|
||||||
|
sortedPeers.append(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
for peer in sortedPeers {
|
||||||
|
let isEnabled = canShareLinkToPeer(peer: peer)
|
||||||
|
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
@ -245,11 +283,12 @@ private struct FolderInviteLinkListControllerState: Equatable {
|
|||||||
var generatingLink: Bool = false
|
var generatingLink: Bool = false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filterId: Int32, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?) -> ViewController {
|
public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filterId: Int32, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) -> ViewController {
|
||||||
var pushControllerImpl: ((ViewController) -> Void)?
|
var pushControllerImpl: ((ViewController) -> Void)?
|
||||||
let _ = pushControllerImpl
|
let _ = pushControllerImpl
|
||||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||||
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
|
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
|
||||||
|
var dismissImpl: (() -> Void)?
|
||||||
|
|
||||||
var dismissTooltipsImpl: (() -> Void)?
|
var dismissTooltipsImpl: (() -> Void)?
|
||||||
|
|
||||||
@ -257,9 +296,6 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
|
|
||||||
var initialState = FolderInviteLinkListControllerState()
|
var initialState = FolderInviteLinkListControllerState()
|
||||||
initialState.currentLink = currentInvitation
|
initialState.currentLink = currentInvitation
|
||||||
for peerId in allPeerIds {
|
|
||||||
initialState.selectedPeerIds.insert(peerId)
|
|
||||||
}
|
|
||||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||||
let stateValue = Atomic(value: initialState)
|
let stateValue = Atomic(value: initialState)
|
||||||
let updateState: ((FolderInviteLinkListControllerState) -> FolderInviteLinkListControllerState) -> Void = { f in
|
let updateState: ((FolderInviteLinkListControllerState) -> FolderInviteLinkListControllerState) -> Void = { f in
|
||||||
@ -275,6 +311,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
|
|
||||||
var getControllerImpl: (() -> ViewController?)?
|
var getControllerImpl: (() -> ViewController?)?
|
||||||
|
|
||||||
|
var displayTooltipImpl: ((UndoOverlayContent) -> Void)?
|
||||||
|
|
||||||
let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in
|
let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in
|
||||||
let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData)
|
let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData)
|
||||||
shareController.completed = { peerIds in
|
shareController.completed = { peerIds in
|
||||||
@ -338,7 +376,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
|
|
||||||
dismissTooltipsImpl?()
|
dismissTooltipsImpl?()
|
||||||
|
|
||||||
UIPasteboard.general.string = invite
|
UIPasteboard.general.string = invite.link
|
||||||
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||||
@ -349,12 +387,33 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
}, action: { _, f in
|
}, action: { _, f in
|
||||||
f(.dismissWithoutContent)
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
//presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, isGroup: isGroup)), nil)
|
presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .chatFolder(slug: invite.slug)), nil)
|
||||||
|
})))
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
|
||||||
|
let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, revoke: true)
|
||||||
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
let _ = (context.engine.peers.revokeChatFolderLink(filterId: filterId, link: invite)
|
||||||
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
linkUpdated(nil)
|
||||||
|
dismissImpl?()
|
||||||
|
})
|
||||||
|
})
|
||||||
})))
|
})))
|
||||||
|
|
||||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||||
presentInGlobalOverlayImpl?(contextController)
|
presentInGlobalOverlayImpl?(contextController)
|
||||||
}, peerAction: { peer in
|
}, peerAction: { peer, isEnabled in
|
||||||
|
let state = stateValue.with({ $0 })
|
||||||
|
if state.currentLink != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEnabled {
|
||||||
updateState { state in
|
updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
|
|
||||||
@ -366,6 +425,18 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
//TODO:localized
|
||||||
|
var text = "You can't invite others here"
|
||||||
|
switch peer {
|
||||||
|
case .channel:
|
||||||
|
text = "You don't have the admin rights to share invite links to this group chat."
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dismissTooltipsImpl?()
|
||||||
|
displayTooltipImpl?(.peers(context: context, peers: [peer], title: nil, text: text, customUndoText: nil))
|
||||||
|
}
|
||||||
}, generateLink: {
|
}, generateLink: {
|
||||||
let currentState = stateValue.with({ $0 })
|
let currentState = stateValue.with({ $0 })
|
||||||
if !currentState.generatingLink {
|
if !currentState.generatingLink {
|
||||||
@ -377,8 +448,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
actionsDisposable.add((context.engine.peers.exportChatFolder(filterId: filterId, title: "Link", peerIds: Array(currentState.selectedPeerIds))
|
actionsDisposable.add((context.engine.peers.exportChatFolder(filterId: filterId, title: "", peerIds: Array(currentState.selectedPeerIds))
|
||||||
|> deliverOnMainQueue).start(next: { result in
|
|> deliverOnMainQueue).start(next: { result in
|
||||||
|
linkUpdated(result)
|
||||||
|
|
||||||
updateState { state in
|
updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
|
|
||||||
@ -392,10 +465,48 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var combinedPeerIds: [EnginePeer.Id] = []
|
||||||
|
if let currentInvitation {
|
||||||
|
for peerId in currentInvitation.peerIds {
|
||||||
|
if !combinedPeerIds.contains(peerId) {
|
||||||
|
combinedPeerIds.append(peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for peerId in allPeerIds {
|
||||||
|
if !combinedPeerIds.contains(peerId) {
|
||||||
|
combinedPeerIds.append(peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let allPeers = context.engine.data.subscribe(
|
let allPeers = context.engine.data.subscribe(
|
||||||
EngineDataList(allPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
EngineDataList(combinedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let _ = (allPeers
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { peers in
|
||||||
|
updateState { state in
|
||||||
|
var state = state
|
||||||
|
|
||||||
|
if let currentInvitation {
|
||||||
|
for peerId in currentInvitation.peerIds {
|
||||||
|
state.selectedPeerIds.insert(peerId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for peerId in allPeerIds {
|
||||||
|
if let peer = peers.first(where: { $0?.id == peerId }), let peerValue = peer {
|
||||||
|
if canShareLinkToPeer(peer: peerValue) {
|
||||||
|
state.selectedPeerIds.insert(peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
||||||
let signal = combineLatest(queue: .mainQueue(),
|
let signal = combineLatest(queue: .mainQueue(),
|
||||||
presentationData,
|
presentationData,
|
||||||
@ -410,7 +521,23 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
let title: ItemListControllerTitle
|
let title: ItemListControllerTitle
|
||||||
title = .text("Share Folder")
|
title = .text("Share Folder")
|
||||||
|
|
||||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
var doneButton: ItemListNavigationButton?
|
||||||
|
doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
|
||||||
|
/*let state = stateValue.with({ $0 })
|
||||||
|
if let currentLink = state.currentLink {
|
||||||
|
updateState { state in
|
||||||
|
var state = state
|
||||||
|
state.isSaving = true
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
actionsDisposable.add(context.engine.peers.editChatFolderLink(filterId: filterId, link: currentLink, title: nil, revoke: false))
|
||||||
|
} else {
|
||||||
|
dismissImpl?()
|
||||||
|
}*/
|
||||||
|
dismissImpl?()
|
||||||
|
})
|
||||||
|
|
||||||
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: doneButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries(
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: folderInviteLinkListControllerEntries(
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
state: state,
|
state: state,
|
||||||
@ -451,9 +578,18 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
|||||||
controller.presentInGlobalOverlay(c)
|
controller.presentInGlobalOverlay(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dismissImpl = { [weak controller] in
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
getControllerImpl = { [weak controller] in
|
getControllerImpl = { [weak controller] in
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
displayTooltipImpl = { [weak controller] c in
|
||||||
|
if let controller = controller {
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||||
|
controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||||
|
}
|
||||||
|
}
|
||||||
dismissTooltipsImpl = { [weak controller] in
|
dismissTooltipsImpl = { [weak controller] in
|
||||||
controller?.window?.forEachController({ controller in
|
controller?.window?.forEachController({ controller in
|
||||||
if let controller = controller as? UndoOverlayController {
|
if let controller = controller as? UndoOverlayController {
|
||||||
|
@ -519,6 +519,13 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
|
|||||||
shimmerNode.updateAbsoluteRect(rect, within: size)
|
shimmerNode.updateAbsoluteRect(rect, within: size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if itemListHasRoundedBlockLayout(params) {
|
||||||
|
shimmerNode.clipsToBounds = true
|
||||||
|
shimmerNode.cornerRadius = 11.0
|
||||||
|
} else {
|
||||||
|
shimmerNode.cornerRadius = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
let lineWidth: CGFloat = 180.0
|
let lineWidth: CGFloat = 180.0
|
||||||
let lineDiameter: CGFloat = 12.0
|
let lineDiameter: CGFloat = 12.0
|
||||||
let titleFrame = strongSelf.invitedPeersNode.frame
|
let titleFrame = strongSelf.invitedPeersNode.frame
|
||||||
|
@ -275,8 +275,8 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte
|
|||||||
let nextColor: ItemBackgroundColor
|
let nextColor: ItemBackgroundColor
|
||||||
let transitionFraction: CGFloat
|
let transitionFraction: CGFloat
|
||||||
|
|
||||||
color = .green
|
color = .blue
|
||||||
nextColor = .yellow
|
nextColor = .blue
|
||||||
transitionFraction = 1.0
|
transitionFraction = 1.0
|
||||||
|
|
||||||
let topColor = color.colors.top
|
let topColor = color.colors.top
|
||||||
|
@ -253,10 +253,12 @@ public enum ItemListPeerItemLabel {
|
|||||||
public struct ItemListPeerItemSwitch {
|
public struct ItemListPeerItemSwitch {
|
||||||
public var value: Bool
|
public var value: Bool
|
||||||
public var style: ItemListPeerItemSwitchStyle
|
public var style: ItemListPeerItemSwitchStyle
|
||||||
|
public var isEnabled: Bool
|
||||||
|
|
||||||
public init(value: Bool, style: ItemListPeerItemSwitchStyle) {
|
public init(value: Bool, style: ItemListPeerItemSwitchStyle, isEnabled: Bool = true) {
|
||||||
self.value = value
|
self.value = value
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.isEnabled = isEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1251,7 +1253,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
if let current = strongSelf.leftCheckNode {
|
if let current = strongSelf.leftCheckNode {
|
||||||
leftCheckNode = current
|
leftCheckNode = current
|
||||||
} else {
|
} else {
|
||||||
leftCheckNode = CheckNode(theme: CheckNodeTheme(theme: item.presentationData.theme, style: .plain))
|
var checkTheme = CheckNodeTheme(theme: item.presentationData.theme, style: .plain)
|
||||||
|
checkTheme.isDottedBorder = !switchValue.isEnabled
|
||||||
|
leftCheckNode = CheckNode(theme: checkTheme)
|
||||||
strongSelf.leftCheckNode = leftCheckNode
|
strongSelf.leftCheckNode = leftCheckNode
|
||||||
strongSelf.avatarNode.supernode?.addSubnode(leftCheckNode)
|
strongSelf.avatarNode.supernode?.addSubnode(leftCheckNode)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ public final class QrCodeScreen: ViewController {
|
|||||||
public enum Subject {
|
public enum Subject {
|
||||||
case peer(peer: EnginePeer)
|
case peer(peer: EnginePeer)
|
||||||
case invite(invite: ExportedInvitation, isGroup: Bool)
|
case invite(invite: ExportedInvitation, isGroup: Bool)
|
||||||
|
case chatFolder(slug: String)
|
||||||
|
|
||||||
var link: String {
|
var link: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -45,6 +46,8 @@ public final class QrCodeScreen: ViewController {
|
|||||||
return "https://t.me/\(peer.addressName ?? "")"
|
return "https://t.me/\(peer.addressName ?? "")"
|
||||||
case let .invite(invite, _):
|
case let .invite(invite, _):
|
||||||
return invite.link ?? ""
|
return invite.link ?? ""
|
||||||
|
case let .chatFolder(slug):
|
||||||
|
return "https://t.me/folder/\(slug)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +57,8 @@ public final class QrCodeScreen: ViewController {
|
|||||||
return "Q"
|
return "Q"
|
||||||
case .invite:
|
case .invite:
|
||||||
return "Q"
|
return "Q"
|
||||||
|
case .chatFolder:
|
||||||
|
return "Q"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[594758406] = { return Api.EncryptedMessage.parse_encryptedMessageService($0) }
|
dict[594758406] = { return Api.EncryptedMessage.parse_encryptedMessageService($0) }
|
||||||
dict[179611673] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) }
|
dict[179611673] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) }
|
||||||
dict[-317687113] = { return Api.ExportedChatInvite.parse_chatInvitePublicJoinRequests($0) }
|
dict[-317687113] = { return Api.ExportedChatInvite.parse_chatInvitePublicJoinRequests($0) }
|
||||||
dict[-1350894801] = { return Api.ExportedCommunityInvite.parse_exportedCommunityInvite($0) }
|
dict[-337788502] = { return Api.ExportedCommunityInvite.parse_exportedCommunityInvite($0) }
|
||||||
dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) }
|
dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) }
|
||||||
dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) }
|
dict[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($0) }
|
||||||
dict[-207944868] = { return Api.FileHash.parse_fileHash($0) }
|
dict[-207944868] = { return Api.FileHash.parse_fileHash($0) }
|
||||||
@ -998,7 +998,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||||||
dict[-191450938] = { return Api.channels.SendAsPeers.parse_sendAsPeers($0) }
|
dict[-191450938] = { return Api.channels.SendAsPeers.parse_sendAsPeers($0) }
|
||||||
dict[1805101290] = { return Api.communities.ExportedCommunityInvite.parse_exportedCommunityInvite($0) }
|
dict[1805101290] = { return Api.communities.ExportedCommunityInvite.parse_exportedCommunityInvite($0) }
|
||||||
dict[-2662489] = { return Api.communities.ExportedInvites.parse_exportedInvites($0) }
|
dict[-2662489] = { return Api.communities.ExportedInvites.parse_exportedInvites($0) }
|
||||||
dict[408604768] = { return Api.community.CommunityInvite.parse_communityInvite($0) }
|
dict[988463765] = { return Api.community.CommunityInvite.parse_communityInvite($0) }
|
||||||
dict[74184410] = { return Api.community.CommunityInvite.parse_communityInviteAlready($0) }
|
dict[74184410] = { return Api.community.CommunityInvite.parse_communityInviteAlready($0) }
|
||||||
dict[182326673] = { return Api.contacts.Blocked.parse_blocked($0) }
|
dict[182326673] = { return Api.contacts.Blocked.parse_blocked($0) }
|
||||||
dict[-513392236] = { return Api.contacts.Blocked.parse_blockedSlice($0) }
|
dict[-513392236] = { return Api.contacts.Blocked.parse_blockedSlice($0) }
|
||||||
|
@ -958,15 +958,16 @@ public extension Api.communities {
|
|||||||
}
|
}
|
||||||
public extension Api.community {
|
public extension Api.community {
|
||||||
enum CommunityInvite: TypeConstructorDescription {
|
enum CommunityInvite: TypeConstructorDescription {
|
||||||
case communityInvite(peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
|
case communityInvite(title: String, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
|
||||||
case communityInviteAlready(filterId: Int32, missingPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
|
case communityInviteAlready(filterId: Int32, missingPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
case .communityInvite(let peers, let chats, let users):
|
case .communityInvite(let title, let peers, let chats, let users):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(408604768)
|
buffer.appendInt32(988463765)
|
||||||
}
|
}
|
||||||
|
serializeString(title, buffer: buffer, boxed: false)
|
||||||
buffer.appendInt32(481674261)
|
buffer.appendInt32(481674261)
|
||||||
buffer.appendInt32(Int32(peers.count))
|
buffer.appendInt32(Int32(peers.count))
|
||||||
for item in peers {
|
for item in peers {
|
||||||
@ -1009,31 +1010,34 @@ public extension Api.community {
|
|||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
switch self {
|
switch self {
|
||||||
case .communityInvite(let peers, let chats, let users):
|
case .communityInvite(let title, let peers, let chats, let users):
|
||||||
return ("communityInvite", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)])
|
return ("communityInvite", [("title", title as Any), ("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)])
|
||||||
case .communityInviteAlready(let filterId, let missingPeers, let chats, let users):
|
case .communityInviteAlready(let filterId, let missingPeers, let chats, let users):
|
||||||
return ("communityInviteAlready", [("filterId", filterId as Any), ("missingPeers", missingPeers as Any), ("chats", chats as Any), ("users", users as Any)])
|
return ("communityInviteAlready", [("filterId", filterId as Any), ("missingPeers", missingPeers as Any), ("chats", chats as Any), ("users", users as Any)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func parse_communityInvite(_ reader: BufferReader) -> CommunityInvite? {
|
public static func parse_communityInvite(_ reader: BufferReader) -> CommunityInvite? {
|
||||||
var _1: [Api.Peer]?
|
var _1: String?
|
||||||
|
_1 = parseString(reader)
|
||||||
|
var _2: [Api.Peer]?
|
||||||
if let _ = reader.readInt32() {
|
if let _ = reader.readInt32() {
|
||||||
_1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self)
|
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self)
|
||||||
}
|
}
|
||||||
var _2: [Api.Chat]?
|
var _3: [Api.Chat]?
|
||||||
if let _ = reader.readInt32() {
|
if let _ = reader.readInt32() {
|
||||||
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||||
}
|
}
|
||||||
var _3: [Api.User]?
|
var _4: [Api.User]?
|
||||||
if let _ = reader.readInt32() {
|
if let _ = reader.readInt32() {
|
||||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||||
}
|
}
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
let _c3 = _3 != nil
|
let _c3 = _3 != nil
|
||||||
if _c1 && _c2 && _c3 {
|
let _c4 = _4 != nil
|
||||||
return Api.community.CommunityInvite.communityInvite(peers: _1!, chats: _2!, users: _3!)
|
if _c1 && _c2 && _c3 && _c4 {
|
||||||
|
return Api.community.CommunityInvite.communityInvite(title: _1!, peers: _2!, chats: _3!, users: _4!)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -1026,14 +1026,15 @@ public extension Api {
|
|||||||
}
|
}
|
||||||
public extension Api {
|
public extension Api {
|
||||||
enum ExportedCommunityInvite: TypeConstructorDescription {
|
enum ExportedCommunityInvite: TypeConstructorDescription {
|
||||||
case exportedCommunityInvite(title: String, url: String, peers: [Api.Peer])
|
case exportedCommunityInvite(flags: Int32, title: String, url: String, peers: [Api.Peer])
|
||||||
|
|
||||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||||
switch self {
|
switch self {
|
||||||
case .exportedCommunityInvite(let title, let url, let peers):
|
case .exportedCommunityInvite(let flags, let title, let url, let peers):
|
||||||
if boxed {
|
if boxed {
|
||||||
buffer.appendInt32(-1350894801)
|
buffer.appendInt32(-337788502)
|
||||||
}
|
}
|
||||||
|
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||||
serializeString(title, buffer: buffer, boxed: false)
|
serializeString(title, buffer: buffer, boxed: false)
|
||||||
serializeString(url, buffer: buffer, boxed: false)
|
serializeString(url, buffer: buffer, boxed: false)
|
||||||
buffer.appendInt32(481674261)
|
buffer.appendInt32(481674261)
|
||||||
@ -1047,25 +1048,28 @@ public extension Api {
|
|||||||
|
|
||||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||||
switch self {
|
switch self {
|
||||||
case .exportedCommunityInvite(let title, let url, let peers):
|
case .exportedCommunityInvite(let flags, let title, let url, let peers):
|
||||||
return ("exportedCommunityInvite", [("title", title as Any), ("url", url as Any), ("peers", peers as Any)])
|
return ("exportedCommunityInvite", [("flags", flags as Any), ("title", title as Any), ("url", url as Any), ("peers", peers as Any)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func parse_exportedCommunityInvite(_ reader: BufferReader) -> ExportedCommunityInvite? {
|
public static func parse_exportedCommunityInvite(_ reader: BufferReader) -> ExportedCommunityInvite? {
|
||||||
var _1: String?
|
var _1: Int32?
|
||||||
_1 = parseString(reader)
|
_1 = reader.readInt32()
|
||||||
var _2: String?
|
var _2: String?
|
||||||
_2 = parseString(reader)
|
_2 = parseString(reader)
|
||||||
var _3: [Api.Peer]?
|
var _3: String?
|
||||||
|
_3 = parseString(reader)
|
||||||
|
var _4: [Api.Peer]?
|
||||||
if let _ = reader.readInt32() {
|
if let _ = reader.readInt32() {
|
||||||
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self)
|
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self)
|
||||||
}
|
}
|
||||||
let _c1 = _1 != nil
|
let _c1 = _1 != nil
|
||||||
let _c2 = _2 != nil
|
let _c2 = _2 != nil
|
||||||
let _c3 = _3 != nil
|
let _c3 = _3 != nil
|
||||||
if _c1 && _c2 && _c3 {
|
let _c4 = _4 != nil
|
||||||
return Api.ExportedCommunityInvite.exportedCommunityInvite(title: _1!, url: _2!, peers: _3!)
|
if _c1 && _c2 && _c3 && _c4 {
|
||||||
|
return Api.ExportedCommunityInvite.exportedCommunityInvite(flags: _1!, title: _2!, url: _3!, peers: _4!)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -15,15 +15,28 @@ public struct ExportedChatFolderLink: Equatable {
|
|||||||
public var title: String
|
public var title: String
|
||||||
public var link: String
|
public var link: String
|
||||||
public var peerIds: [EnginePeer.Id]
|
public var peerIds: [EnginePeer.Id]
|
||||||
|
public var isRevoked: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
link: String,
|
link: String,
|
||||||
peerIds: [EnginePeer.Id]
|
peerIds: [EnginePeer.Id],
|
||||||
|
isRevoked: Bool
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.link = link
|
self.link = link
|
||||||
self.peerIds = peerIds
|
self.peerIds = peerIds
|
||||||
|
self.isRevoked = isRevoked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ExportedChatFolderLink {
|
||||||
|
var slug: String {
|
||||||
|
var slug = self.link
|
||||||
|
if slug.hasPrefix("https://t.me/folder/") {
|
||||||
|
slug = String(slug[slug.index(slug.startIndex, offsetBy: "https://t.me/folder/".count)...])
|
||||||
|
}
|
||||||
|
return slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,11 +68,12 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String
|
|||||||
})
|
})
|
||||||
|
|
||||||
switch invite {
|
switch invite {
|
||||||
case let .exportedCommunityInvite(title, url, peers):
|
case let .exportedCommunityInvite(flags, title, url, peers):
|
||||||
return .single(ExportedChatFolderLink(
|
return .single(ExportedChatFolderLink(
|
||||||
title: title,
|
title: title,
|
||||||
link: url,
|
link: url,
|
||||||
peerIds: peers.map(\.peerId)
|
peerIds: peers.map(\.peerId),
|
||||||
|
isRevoked: (flags & (1 << 0)) != 0
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +84,7 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func _internal_getExportedChatLinks(account: Account, id: Int32) -> Signal<[ExportedChatFolderLink], NoError> {
|
func _internal_getExportedChatFolderLinks(account: Account, id: Int32) -> Signal<[ExportedChatFolderLink], NoError> {
|
||||||
return account.network.request(Api.functions.communities.getExportedInvites(community: .inputCommunityDialogFilter(filterId: id)))
|
return account.network.request(Api.functions.communities.getExportedInvites(community: .inputCommunityDialogFilter(filterId: id)))
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|> `catch` { _ -> Signal<Api.communities.ExportedInvites?, NoError> in
|
|> `catch` { _ -> Signal<Api.communities.ExportedInvites?, NoError> in
|
||||||
@ -105,8 +119,13 @@ func _internal_getExportedChatLinks(account: Account, id: Int32) -> Signal<[Expo
|
|||||||
var result: [ExportedChatFolderLink] = []
|
var result: [ExportedChatFolderLink] = []
|
||||||
for invite in invites {
|
for invite in invites {
|
||||||
switch invite {
|
switch invite {
|
||||||
case let .exportedCommunityInvite(title, url, peers):
|
case let .exportedCommunityInvite(flags, title, url, peers):
|
||||||
result.append(ExportedChatFolderLink(title: title, link: url, peerIds: peers.map(\.peerId)))
|
result.append(ExportedChatFolderLink(
|
||||||
|
title: title,
|
||||||
|
link: url,
|
||||||
|
peerIds: peers.map(\.peerId),
|
||||||
|
isRevoked: (flags & (1 << 0)) != 0
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,3 +134,171 @@ func _internal_getExportedChatLinks(account: Account, id: Int32) -> Signal<[Expo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum EditChatFolderLinkError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_editChatFolderLink(account: Account, filterId: Int32, link: ExportedChatFolderLink, title: String?, revoke: Bool) -> Signal<Never, EditChatFolderLinkError> {
|
||||||
|
var flags: Int32 = 0
|
||||||
|
if revoke {
|
||||||
|
flags |= 1 << 0
|
||||||
|
}
|
||||||
|
if title != nil {
|
||||||
|
flags |= 1 << 1
|
||||||
|
}
|
||||||
|
return account.network.request(Api.functions.communities.editExportedInvite(flags: flags, community: .inputCommunityDialogFilter(filterId: filterId), slug: link.slug, title: title))
|
||||||
|
|> mapError { _ -> EditChatFolderLinkError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RevokeChatFolderLinkError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_revokeChatFolderLink(account: Account, filterId: Int32, link: ExportedChatFolderLink) -> Signal<Never, RevokeChatFolderLinkError> {
|
||||||
|
return account.network.request(Api.functions.communities.deleteExportedInvite(community: .inputCommunityDialogFilter(filterId: filterId), slug: link.slug))
|
||||||
|
|> mapError { _ -> RevokeChatFolderLinkError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CheckChatFolderLinkError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class ChatFolderLinkContents {
|
||||||
|
public let localFilterId: Int32?
|
||||||
|
public let title: String?
|
||||||
|
public let peers: [EnginePeer]
|
||||||
|
public let alreadyMemberPeerIds: Set<EnginePeer.Id>
|
||||||
|
|
||||||
|
public init(
|
||||||
|
localFilterId: Int32?,
|
||||||
|
title: String?,
|
||||||
|
peers: [EnginePeer],
|
||||||
|
alreadyMemberPeerIds: Set<EnginePeer.Id>
|
||||||
|
) {
|
||||||
|
self.localFilterId = localFilterId
|
||||||
|
self.title = title
|
||||||
|
self.peers = peers
|
||||||
|
self.alreadyMemberPeerIds = alreadyMemberPeerIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<ChatFolderLinkContents, CheckChatFolderLinkError> {
|
||||||
|
return account.network.request(Api.functions.communities.checkCommunityInvite(slug: slug))
|
||||||
|
|> mapError { _ -> CheckChatFolderLinkError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<ChatFolderLinkContents, CheckChatFolderLinkError> in
|
||||||
|
return account.postbox.transaction { transaction -> ChatFolderLinkContents in
|
||||||
|
switch result {
|
||||||
|
case let .communityInvite(title, peers, chats, users):
|
||||||
|
var allPeers: [Peer] = []
|
||||||
|
var peerPresences: [PeerId: Api.User] = [:]
|
||||||
|
|
||||||
|
for user in users {
|
||||||
|
let telegramUser = TelegramUser(user: user)
|
||||||
|
allPeers.append(telegramUser)
|
||||||
|
peerPresences[telegramUser.id] = user
|
||||||
|
}
|
||||||
|
for chat in chats {
|
||||||
|
if let peer = parseTelegramGroupOrChannel(chat: chat) {
|
||||||
|
allPeers.append(peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||||
|
|
||||||
|
var resultPeers: [EnginePeer] = []
|
||||||
|
var alreadyMemberPeerIds = Set<EnginePeer.Id>()
|
||||||
|
for peer in peers {
|
||||||
|
if let peerValue = transaction.getPeer(peer.peerId) {
|
||||||
|
resultPeers.append(EnginePeer(peerValue))
|
||||||
|
|
||||||
|
if transaction.getPeerChatListIndex(peer.peerId) != nil {
|
||||||
|
alreadyMemberPeerIds.insert(peer.peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds)
|
||||||
|
case let .communityInviteAlready(filterId, missingPeers, chats, users):
|
||||||
|
var allPeers: [Peer] = []
|
||||||
|
var peerPresences: [PeerId: Api.User] = [:]
|
||||||
|
|
||||||
|
for user in users {
|
||||||
|
let telegramUser = TelegramUser(user: user)
|
||||||
|
allPeers.append(telegramUser)
|
||||||
|
peerPresences[telegramUser.id] = user
|
||||||
|
}
|
||||||
|
for chat in chats {
|
||||||
|
if let peer = parseTelegramGroupOrChannel(chat: chat) {
|
||||||
|
allPeers.append(peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
|
||||||
|
|
||||||
|
let currentFilters = _internal_currentChatListFilters(transaction: transaction)
|
||||||
|
var currentFilterTitle: String?
|
||||||
|
if let index = currentFilters.firstIndex(where: { $0.id == filterId }) {
|
||||||
|
switch currentFilters[index] {
|
||||||
|
case let .filter(_, title, _, _):
|
||||||
|
currentFilterTitle = title
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultPeers: [EnginePeer] = []
|
||||||
|
var alreadyMemberPeerIds = Set<EnginePeer.Id>()
|
||||||
|
for peer in missingPeers {
|
||||||
|
if let peerValue = transaction.getPeer(peer.peerId) {
|
||||||
|
resultPeers.append(EnginePeer(peerValue))
|
||||||
|
|
||||||
|
if transaction.getPeerChatListIndex(peer.peerId) != nil {
|
||||||
|
alreadyMemberPeerIds.insert(peer.peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> castError(CheckChatFolderLinkError.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum JoinChatFolderLinkError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> {
|
||||||
|
return account.postbox.transaction { transaction -> [Api.InputPeer] in
|
||||||
|
return peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer)
|
||||||
|
}
|
||||||
|
|> castError(JoinChatFolderLinkError.self)
|
||||||
|
|> mapToSignal { inputPeers -> Signal<Never, JoinChatFolderLinkError> in
|
||||||
|
return account.network.request(Api.functions.communities.joinCommunityInvite(slug: slug, peers: inputPeers))
|
||||||
|
|> mapError { _ -> JoinChatFolderLinkError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<Never, JoinChatFolderLinkError> in
|
||||||
|
account.stateManager.addUpdates(result)
|
||||||
|
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1030,8 +1030,24 @@ public extension TelegramEngine {
|
|||||||
return _internal_exportChatFolder(account: self.account, filterId: filterId, title: title, peerIds: peerIds)
|
return _internal_exportChatFolder(account: self.account, filterId: filterId, title: title, peerIds: peerIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getExportedChatLinks(id: Int32) -> Signal<[ExportedChatFolderLink], NoError> {
|
public func getExportedChatFolderLinks(id: Int32) -> Signal<[ExportedChatFolderLink], NoError> {
|
||||||
return _internal_getExportedChatLinks(account: self.account, id: id)
|
return _internal_getExportedChatFolderLinks(account: self.account, id: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func editChatFolderLink(filterId: Int32, link: ExportedChatFolderLink, title: String?, revoke: Bool) -> Signal<Never, EditChatFolderLinkError> {
|
||||||
|
return _internal_editChatFolderLink(account: self.account, filterId: filterId, link: link, title: title, revoke: revoke)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func revokeChatFolderLink(filterId: Int32, link: ExportedChatFolderLink) -> Signal<Never, RevokeChatFolderLinkError> {
|
||||||
|
return _internal_revokeChatFolderLink(account: self.account, filterId: filterId, link: link)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func checkChatFolderLink(slug: String) -> Signal<ChatFolderLinkContents, CheckChatFolderLinkError> {
|
||||||
|
return _internal_checkChatFolderLink(account: self.account, slug: slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> {
|
||||||
|
return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,6 +358,7 @@ swift_library(
|
|||||||
"//submodules/DrawingUI:DrawingUI",
|
"//submodules/DrawingUI:DrawingUI",
|
||||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||||
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
|
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
|
||||||
|
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
|
||||||
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
|
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
|
||||||
] + select({
|
] + select({
|
||||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "ChatFolderLinkPreviewScreen",
|
||||||
|
module_name = "ChatFolderLinkPreviewScreen",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/AsyncDisplayKit",
|
||||||
|
"//submodules/Display",
|
||||||
|
"//submodules/Postbox",
|
||||||
|
"//submodules/TelegramCore",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
|
"//submodules/ComponentFlow",
|
||||||
|
"//submodules/Components/ViewControllerComponent",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/Components/MultilineTextComponent",
|
||||||
|
"//submodules/TelegramPresentationData",
|
||||||
|
"//submodules/AccountContext",
|
||||||
|
"//submodules/AppBundle",
|
||||||
|
"//submodules/TelegramStringFormatting",
|
||||||
|
"//submodules/PresentationDataUtils",
|
||||||
|
"//submodules/Components/SolidRoundedButtonComponent",
|
||||||
|
"//submodules/AvatarNode",
|
||||||
|
"//submodules/CheckNode",
|
||||||
|
"//submodules/Markdown",
|
||||||
|
"//submodules/UndoUI",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,187 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import AccountContext
|
||||||
|
import MultilineTextComponent
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
final class ChatFolderLinkHeaderComponent: Component {
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let title: String
|
||||||
|
let badge: String?
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
title: String,
|
||||||
|
badge: String?
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.title = title
|
||||||
|
self.badge = badge
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: ChatFolderLinkHeaderComponent, rhs: ChatFolderLinkHeaderComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.badge != rhs.badge {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let leftView = UIImageView()
|
||||||
|
private let rightView = UIImageView()
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
private var component: ChatFolderLinkHeaderComponent?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.leftView)
|
||||||
|
self.addSubview(self.rightView)
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
|
||||||
|
self.separatorLayer.cornerRadius = 2.0
|
||||||
|
self.separatorLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: ChatFolderLinkHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
let height: CGFloat = 60.0
|
||||||
|
let spacing: CGFloat = 16.0
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
//TODO:localize
|
||||||
|
let leftString = NSAttributedString(string: "All Chats", font: Font.semibold(14.0), textColor: component.theme.list.freeTextColor)
|
||||||
|
let rightString = NSAttributedString(string: "Personal", font: Font.semibold(14.0), textColor: component.theme.list.freeTextColor)
|
||||||
|
|
||||||
|
let leftStringBounds = leftString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||||
|
let rightStringBounds = rightString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||||
|
|
||||||
|
self.leftView.image = generateImage(leftStringBounds.size.integralFloor, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
|
||||||
|
leftString.draw(in: leftStringBounds)
|
||||||
|
|
||||||
|
var colors: [UIColor] = []
|
||||||
|
var locations: [CGFloat] = []
|
||||||
|
for i in 0 ... 8 {
|
||||||
|
let t: CGFloat = CGFloat(i) / CGFloat(8)
|
||||||
|
let a: CGFloat = t * t
|
||||||
|
colors.append(UIColor(white: 1.0, alpha: a))
|
||||||
|
locations.append(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = generateGradientImage(size: CGSize(width: size.width * 0.8, height: 16.0), colors: colors, locations: locations, direction: .horizontal) {
|
||||||
|
image.draw(in: CGRect(origin: CGPoint(), size: CGSize(width: image.size.width, height: size.height)), blendMode: .destinationIn, alpha: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
})
|
||||||
|
self.leftView.alpha = 0.5
|
||||||
|
|
||||||
|
self.rightView.image = generateImage(rightStringBounds.size.integralFloor, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
|
||||||
|
rightString.draw(in: rightStringBounds)
|
||||||
|
|
||||||
|
var colors: [UIColor] = []
|
||||||
|
var locations: [CGFloat] = []
|
||||||
|
for i in 0 ... 8 {
|
||||||
|
let t: CGFloat = CGFloat(i) / CGFloat(8)
|
||||||
|
let a: CGFloat = 1.0 - t * t
|
||||||
|
colors.append(UIColor(white: 1.0, alpha: a))
|
||||||
|
locations.append(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = generateGradientImage(size: CGSize(width: size.width * 0.8, height: 16.0), colors: colors, locations: locations, direction: .horizontal) {
|
||||||
|
image.draw(in: CGRect(origin: CGPoint(x: size.width - image.size.width, y: 0.0), size: CGSize(width: image.size.width, height: size.height)), blendMode: .destinationIn, alpha: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
})
|
||||||
|
self.rightView.alpha = 0.5
|
||||||
|
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentWidth: CGFloat = 0.0
|
||||||
|
if let leftImage = self.leftView.image {
|
||||||
|
contentWidth += leftImage.size.width
|
||||||
|
}
|
||||||
|
contentWidth += spacing
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.list.itemAccentColor)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 200.0, height: 100.0)
|
||||||
|
)
|
||||||
|
contentWidth += titleSize.width
|
||||||
|
|
||||||
|
contentWidth += spacing
|
||||||
|
if let rightImage = self.rightView.image {
|
||||||
|
contentWidth += rightImage.size.width
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentOriginX: CGFloat = 0.0
|
||||||
|
|
||||||
|
if let leftImage = self.leftView.image {
|
||||||
|
transition.setFrame(view: self.leftView, frame: CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - leftImage.size.height) / 2.0) - 1.0), size: leftImage.size))
|
||||||
|
contentOriginX += leftImage.size.width
|
||||||
|
}
|
||||||
|
contentOriginX += spacing
|
||||||
|
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
transition.setFrame(view: titleView, frame: titleFrame)
|
||||||
|
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + 9.0), size: CGSize(width: titleFrame.width, height: 3.0)))
|
||||||
|
}
|
||||||
|
contentOriginX += titleSize.width
|
||||||
|
contentOriginX += spacing
|
||||||
|
|
||||||
|
if let rightImage = self.rightView.image {
|
||||||
|
transition.setFrame(view: self.rightView, frame: CGRect(origin: CGPoint(x: contentOriginX, y: floor((height - rightImage.size.height) / 2.0) - 1.0), size: rightImage.size))
|
||||||
|
contentOriginX += rightImage.size.width
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: contentWidth, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,721 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
|
import ViewControllerComponent
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import MultilineTextComponent
|
||||||
|
import Postbox
|
||||||
|
import SolidRoundedButtonComponent
|
||||||
|
import PresentationDataUtils
|
||||||
|
import Markdown
|
||||||
|
import UndoUI
|
||||||
|
|
||||||
|
private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||||
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let slug: String
|
||||||
|
let linkContents: ChatFolderLinkContents?
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
slug: String,
|
||||||
|
linkContents: ChatFolderLinkContents?
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.slug = slug
|
||||||
|
self.linkContents = linkContents
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: ChatFolderLinkPreviewScreenComponent, rhs: ChatFolderLinkPreviewScreenComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.slug != rhs.slug {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.linkContents !== rhs.linkContents {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ItemLayout: Equatable {
|
||||||
|
var containerSize: CGSize
|
||||||
|
var containerInset: CGFloat
|
||||||
|
var bottomInset: CGFloat
|
||||||
|
var topInset: CGFloat
|
||||||
|
|
||||||
|
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
|
||||||
|
self.containerSize = containerSize
|
||||||
|
self.containerInset = containerInset
|
||||||
|
self.bottomInset = bottomInset
|
||||||
|
self.topInset = topInset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ScrollView: UIScrollView {
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView, UIScrollViewDelegate {
|
||||||
|
private let dimView: UIView
|
||||||
|
private let backgroundLayer: SimpleLayer
|
||||||
|
private let navigationBarContainer: SparseContainerView
|
||||||
|
private let scrollView: ScrollView
|
||||||
|
private let scrollContentClippingView: SparseContainerView
|
||||||
|
private let scrollContentView: UIView
|
||||||
|
|
||||||
|
private let topIcon = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let leftButton = ComponentView<Empty>()
|
||||||
|
private let descriptionText = ComponentView<Empty>()
|
||||||
|
private let actionButton = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private let listHeaderText = ComponentView<Empty>()
|
||||||
|
private let itemContainerView: UIView
|
||||||
|
private var items: [AnyHashable: ComponentView<Empty>] = [:]
|
||||||
|
|
||||||
|
private var selectedItems = Set<EnginePeer.Id>()
|
||||||
|
|
||||||
|
private let bottomOverscrollLimit: CGFloat
|
||||||
|
|
||||||
|
private var ignoreScrolling: Bool = false
|
||||||
|
|
||||||
|
private var component: ChatFolderLinkPreviewScreenComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
private var environment: ViewControllerComponentContainer.Environment?
|
||||||
|
private var itemLayout: ItemLayout?
|
||||||
|
|
||||||
|
private var topOffsetDistance: CGFloat?
|
||||||
|
|
||||||
|
private var joinDisposable: Disposable?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.bottomOverscrollLimit = 200.0
|
||||||
|
|
||||||
|
self.dimView = UIView()
|
||||||
|
|
||||||
|
self.backgroundLayer = SimpleLayer()
|
||||||
|
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
self.backgroundLayer.cornerRadius = 10.0
|
||||||
|
|
||||||
|
self.navigationBarContainer = SparseContainerView()
|
||||||
|
|
||||||
|
self.scrollView = ScrollView()
|
||||||
|
|
||||||
|
self.scrollContentClippingView = SparseContainerView()
|
||||||
|
self.scrollContentClippingView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.scrollContentView = UIView()
|
||||||
|
|
||||||
|
self.itemContainerView = UIView()
|
||||||
|
self.itemContainerView.clipsToBounds = true
|
||||||
|
self.itemContainerView.layer.cornerRadius = 10.0
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.dimView)
|
||||||
|
self.layer.addSublayer(self.backgroundLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.navigationBarContainer)
|
||||||
|
|
||||||
|
self.scrollView.delaysContentTouches = true
|
||||||
|
self.scrollView.canCancelContentTouches = true
|
||||||
|
self.scrollView.clipsToBounds = false
|
||||||
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||||
|
}
|
||||||
|
self.scrollView.showsVerticalScrollIndicator = false
|
||||||
|
self.scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
self.scrollView.alwaysBounceHorizontal = false
|
||||||
|
self.scrollView.alwaysBounceVertical = true
|
||||||
|
self.scrollView.scrollsToTop = false
|
||||||
|
self.scrollView.delegate = self
|
||||||
|
self.scrollView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.addSubview(self.scrollContentClippingView)
|
||||||
|
self.scrollContentClippingView.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
self.scrollView.addSubview(self.scrollContentView)
|
||||||
|
|
||||||
|
self.scrollContentView.addSubview(self.itemContainerView)
|
||||||
|
|
||||||
|
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.joinDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if !self.ignoreScrolling {
|
||||||
|
self.updateScrolling(transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
|
guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
||||||
|
topOffset = max(0.0, topOffset)
|
||||||
|
|
||||||
|
if topOffset < topOffsetDistance {
|
||||||
|
targetContentOffset.pointee.y = scrollView.contentOffset.y
|
||||||
|
scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
if !self.bounds.contains(point) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !self.backgroundLayer.frame.contains(point) {
|
||||||
|
return self.dimView
|
||||||
|
}
|
||||||
|
|
||||||
|
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = super.hitTest(point, with: event)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
guard let environment = self.environment, let controller = environment.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScrolling(transition: Transition) {
|
||||||
|
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
||||||
|
topOffset = max(0.0, topOffset)
|
||||||
|
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
|
||||||
|
|
||||||
|
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
|
||||||
|
|
||||||
|
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
|
||||||
|
self.topOffsetDistance = topOffsetDistance
|
||||||
|
var topOffsetFraction = topOffset / topOffsetDistance
|
||||||
|
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
|
||||||
|
|
||||||
|
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
|
||||||
|
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
|
let animateOffset: CGFloat = self.backgroundLayer.frame.minY
|
||||||
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
if let actionButtonView = self.actionButton.view {
|
||||||
|
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
|
let animateOffset: CGFloat = self.backgroundLayer.frame.minY
|
||||||
|
|
||||||
|
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||||
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
|
if let actionButtonView = self.actionButton.view {
|
||||||
|
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||||
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||||
|
let themeUpdated = self.environment?.theme !== environment.theme
|
||||||
|
|
||||||
|
let resetScrolling = self.scrollView.bounds.width != availableSize.width
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
if self.component?.linkContents == nil, let linkContents = component.linkContents {
|
||||||
|
for peer in linkContents.peers {
|
||||||
|
self.selectedItems.insert(peer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||||
|
self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
|
||||||
|
self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
|
var contentHeight: CGFloat = 0.0
|
||||||
|
|
||||||
|
let leftButtonSize = self.leftButton.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Button(
|
||||||
|
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)),
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 56.0))),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 120.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: leftButtonSize)
|
||||||
|
if let leftButtonView = self.leftButton.view {
|
||||||
|
if leftButtonView.superview == nil {
|
||||||
|
self.navigationBarContainer.addSubview(leftButtonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: leftButtonView, frame: leftButtonFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleString: String
|
||||||
|
if let linkContents = component.linkContents {
|
||||||
|
//TODO:localize
|
||||||
|
if linkContents.localFilterId != nil {
|
||||||
|
if self.selectedItems.count == 1 {
|
||||||
|
titleString = "Add \(self.selectedItems.count) chat"
|
||||||
|
} else {
|
||||||
|
titleString = "Add \(self.selectedItems.count) chats"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleString = "Add Folder"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleString = " "
|
||||||
|
}
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 18.0), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
self.navigationBarContainer.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: titleView, frame: titleFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += 44.0
|
||||||
|
contentHeight += 14.0
|
||||||
|
|
||||||
|
let topIconSize = self.topIcon.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(ChatFolderLinkHeaderComponent(
|
||||||
|
theme: environment.theme,
|
||||||
|
strings: environment.strings,
|
||||||
|
title: component.linkContents?.title ?? "Folder",
|
||||||
|
badge: nil
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0)
|
||||||
|
)
|
||||||
|
let topIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topIconSize.width) * 0.5), y: contentHeight), size: topIconSize)
|
||||||
|
if let topIconView = self.topIcon.view {
|
||||||
|
if topIconView.superview == nil {
|
||||||
|
self.scrollContentView.addSubview(topIconView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: topIconView, frame: topIconFrame)
|
||||||
|
topIconView.isHidden = component.linkContents == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += topIconSize.height
|
||||||
|
contentHeight += 20.0
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
if let linkContents = component.linkContents {
|
||||||
|
if linkContents.localFilterId == nil {
|
||||||
|
text = "Do you want to add a new chat folder\nand join its groups and channels?"
|
||||||
|
} else {
|
||||||
|
let chatCountString: String
|
||||||
|
if self.selectedItems.count == 1 {
|
||||||
|
chatCountString = "1 chat"
|
||||||
|
} else {
|
||||||
|
chatCountString = "\(self.selectedItems.count) chats"
|
||||||
|
}
|
||||||
|
if let title = linkContents.title {
|
||||||
|
text = "Do you want to add **\(chatCountString)** to your\nfolder **\(title)**?"
|
||||||
|
} else {
|
||||||
|
text = "Do you want to add **\(chatCountString)** chats to your\nfolder?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor)
|
||||||
|
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor)
|
||||||
|
|
||||||
|
let descriptionTextSize = self.descriptionText.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .markdown(text: text, attributes: MarkdownAttributes(
|
||||||
|
body: body,
|
||||||
|
bold: bold,
|
||||||
|
link: body,
|
||||||
|
linkAttribute: { _ in nil }
|
||||||
|
)),
|
||||||
|
horizontalAlignment: .center,
|
||||||
|
maximumNumberOfLines: 0
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize)
|
||||||
|
if let descriptionTextView = self.descriptionText.view {
|
||||||
|
if descriptionTextView.superview == nil {
|
||||||
|
self.scrollContentView.addSubview(descriptionTextView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += descriptionTextFrame.height
|
||||||
|
contentHeight += 39.0
|
||||||
|
|
||||||
|
var singleItemHeight: CGFloat = 0.0
|
||||||
|
|
||||||
|
var itemsHeight: CGFloat = 0.0
|
||||||
|
var validIds: [AnyHashable] = []
|
||||||
|
if let linkContents = component.linkContents {
|
||||||
|
for i in 0 ..< linkContents.peers.count {
|
||||||
|
let peer = linkContents.peers[i]
|
||||||
|
|
||||||
|
for _ in 0 ..< 1 {
|
||||||
|
//let id: AnyHashable = AnyHashable("\(peer.id)_\(j)")
|
||||||
|
let id = AnyHashable(peer.id)
|
||||||
|
validIds.append(id)
|
||||||
|
|
||||||
|
let item: ComponentView<Empty>
|
||||||
|
var itemTransition = transition
|
||||||
|
if let current = self.items[id] {
|
||||||
|
item = current
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
item = ComponentView()
|
||||||
|
self.items[id] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemSize = item.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(PeerListItemComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: environment.theme,
|
||||||
|
strings: environment.strings,
|
||||||
|
sideInset: 0.0,
|
||||||
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
|
peer: peer,
|
||||||
|
subtitle: nil,
|
||||||
|
selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)),
|
||||||
|
hasNext: i != linkContents.peers.count - 1,
|
||||||
|
action: { [weak self] peer in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.selectedItems.contains(peer.id) {
|
||||||
|
self.selectedItems.remove(peer.id)
|
||||||
|
} else {
|
||||||
|
self.selectedItems.insert(peer.id)
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
||||||
|
|
||||||
|
if let itemView = item.view {
|
||||||
|
if itemView.superview == nil {
|
||||||
|
self.itemContainerView.addSubview(itemView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsHeight += itemSize.height
|
||||||
|
singleItemHeight = itemSize.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeIds: [AnyHashable] = []
|
||||||
|
for (id, item) in self.items {
|
||||||
|
if !validIds.contains(id) {
|
||||||
|
removeIds.append(id)
|
||||||
|
item.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removeIds {
|
||||||
|
self.items.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let listHeaderTitle: String
|
||||||
|
if self.selectedItems.count == 1 {
|
||||||
|
listHeaderTitle = "1 CHAT IN FOLDER TO JOIN"
|
||||||
|
} else {
|
||||||
|
listHeaderTitle = "\(self.selectedItems.count) CHATS IN FOLDER TO JOIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor)
|
||||||
|
|
||||||
|
let listHeaderTextSize = self.listHeaderText.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .markdown(
|
||||||
|
text: listHeaderTitle,
|
||||||
|
attributes: MarkdownAttributes(
|
||||||
|
body: listHeaderBody,
|
||||||
|
bold: listHeaderBody,
|
||||||
|
link: listHeaderBody,
|
||||||
|
linkAttribute: { _ in nil }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
if let listHeaderTextView = self.listHeaderText.view {
|
||||||
|
if listHeaderTextView.superview == nil {
|
||||||
|
listHeaderTextView.layer.anchorPoint = CGPoint()
|
||||||
|
self.scrollContentView.addSubview(listHeaderTextView)
|
||||||
|
}
|
||||||
|
let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize)
|
||||||
|
transition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin)
|
||||||
|
listHeaderTextView.bounds = CGRect(origin: CGPoint(), size: listHeaderTextFrame.size)
|
||||||
|
listHeaderTextView.isHidden = component.linkContents == nil
|
||||||
|
}
|
||||||
|
contentHeight += listHeaderTextSize.height
|
||||||
|
contentHeight += 6.0
|
||||||
|
|
||||||
|
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight)))
|
||||||
|
|
||||||
|
var initialContentHeight = contentHeight
|
||||||
|
initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5))
|
||||||
|
|
||||||
|
contentHeight += itemsHeight
|
||||||
|
contentHeight += 24.0
|
||||||
|
initialContentHeight += 24.0
|
||||||
|
|
||||||
|
let actionButtonTitle: String
|
||||||
|
if let linkContents = component.linkContents {
|
||||||
|
if linkContents.localFilterId != nil {
|
||||||
|
actionButtonTitle = "Join Chats"
|
||||||
|
} else {
|
||||||
|
actionButtonTitle = "Add Folder"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionButtonTitle = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionButtonSize = self.actionButton.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(SolidRoundedButtonComponent(
|
||||||
|
title: actionButtonTitle,
|
||||||
|
badge: (self.selectedItems.isEmpty) ? nil : "\(self.selectedItems.count)",
|
||||||
|
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
||||||
|
font: .bold,
|
||||||
|
fontSize: 17.0,
|
||||||
|
height: 50.0,
|
||||||
|
cornerRadius: 11.0,
|
||||||
|
gloss: false,
|
||||||
|
isEnabled: !self.selectedItems.isEmpty,
|
||||||
|
animationName: nil,
|
||||||
|
iconPosition: .right,
|
||||||
|
iconSpacing: 4.0,
|
||||||
|
isLoading: component.linkContents == nil,
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let _ = component.linkContents {
|
||||||
|
if self.joinDisposable == nil, !self.selectedItems.isEmpty {
|
||||||
|
self.joinDisposable = (component.context.engine.peers.joinChatFolderLink(slug: component.slug, peerIds: Array(self.selectedItems))
|
||||||
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
|
guard let self, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.dismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if self.selectedItems.isEmpty {
|
||||||
|
controller.dismiss()
|
||||||
|
} else if let link = component.link {
|
||||||
|
let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) }
|
||||||
|
|
||||||
|
let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
|
||||||
|
let text: String
|
||||||
|
if selectedPeers.count == 1 {
|
||||||
|
text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
|
||||||
|
} else if selectedPeers.count == 2 {
|
||||||
|
text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
|
||||||
|
} else {
|
||||||
|
text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
|
||||||
|
|
||||||
|
controller.dismiss()
|
||||||
|
} else {
|
||||||
|
controller.dismiss()
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||||
|
)
|
||||||
|
let bottomPanelHeight = 14.0 + environment.safeInsets.bottom + actionButtonSize.height
|
||||||
|
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||||
|
if let actionButtonView = self.actionButton.view {
|
||||||
|
if actionButtonView.superview == nil {
|
||||||
|
self.addSubview(actionButtonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += bottomPanelHeight
|
||||||
|
initialContentHeight += bottomPanelHeight
|
||||||
|
|
||||||
|
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
||||||
|
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
||||||
|
|
||||||
|
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
|
||||||
|
|
||||||
|
self.scrollContentClippingView.layer.cornerRadius = 10.0
|
||||||
|
|
||||||
|
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
|
||||||
|
|
||||||
|
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
|
||||||
|
|
||||||
|
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
||||||
|
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
|
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset + 56.0)))
|
||||||
|
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
||||||
|
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
||||||
|
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset)))
|
||||||
|
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
||||||
|
if contentSize != self.scrollView.contentSize {
|
||||||
|
self.scrollView.contentSize = contentSize
|
||||||
|
}
|
||||||
|
if resetScrolling {
|
||||||
|
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
|
||||||
|
}
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
self.updateScrolling(transition: transition)
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
|
||||||
|
private let context: AccountContext
|
||||||
|
private var linkContents: ChatFolderLinkContents?
|
||||||
|
private var linkContentsDisposable: Disposable?
|
||||||
|
|
||||||
|
private var isDismissed: Bool = false
|
||||||
|
|
||||||
|
public init(context: AccountContext, slug: String) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init(context: context, component: ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: nil), navigationBarAppearance: .none)
|
||||||
|
|
||||||
|
self.statusBar.statusBarStyle = .Ignore
|
||||||
|
self.navigationPresentation = .flatModal
|
||||||
|
self.blocksBackgroundWhenInOverlay = true
|
||||||
|
|
||||||
|
self.linkContentsDisposable = (context.engine.peers.checkChatFolderLink(slug: slug)
|
||||||
|
//|> delay(0.2, queue: .mainQueue())
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.linkContents = result
|
||||||
|
self.updateComponent(component: AnyComponent(ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: result)), transition: .immediate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.linkContentsDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
self.view.disablesInteractiveModalDismiss = true
|
||||||
|
|
||||||
|
if let componentView = self.node.hostView.componentView as? ChatFolderLinkPreviewScreenComponent.View {
|
||||||
|
componentView.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||||
|
if !self.isDismissed {
|
||||||
|
self.isDismissed = true
|
||||||
|
|
||||||
|
if let componentView = self.node.hostView.componentView as? ChatFolderLinkPreviewScreenComponent.View {
|
||||||
|
componentView.animateOut(completion: { [weak self] in
|
||||||
|
completion?()
|
||||||
|
self?.dismiss(animated: false)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.dismiss(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,327 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
import TelegramCore
|
||||||
|
import MultilineTextComponent
|
||||||
|
import Postbox
|
||||||
|
import AvatarNode
|
||||||
|
import TelegramPresentationData
|
||||||
|
import CheckNode
|
||||||
|
import TelegramStringFormatting
|
||||||
|
|
||||||
|
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||||
|
|
||||||
|
private func cancelContextGestures(view: UIView) {
|
||||||
|
if let gestureRecognizers = view.gestureRecognizers {
|
||||||
|
for gesture in gestureRecognizers {
|
||||||
|
if let gesture = gesture as? ContextGesture {
|
||||||
|
gesture.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
cancelContextGestures(view: subview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PeerListItemComponent: Component {
|
||||||
|
enum SelectionState: Equatable {
|
||||||
|
case none
|
||||||
|
case editing(isSelected: Bool, isTinted: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let sideInset: CGFloat
|
||||||
|
let title: String
|
||||||
|
let peer: EnginePeer?
|
||||||
|
let subtitle: String?
|
||||||
|
let selectionState: SelectionState
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: (EnginePeer) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
sideInset: CGFloat,
|
||||||
|
title: String,
|
||||||
|
peer: EnginePeer?,
|
||||||
|
subtitle: String?,
|
||||||
|
selectionState: SelectionState,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping (EnginePeer) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.title = title
|
||||||
|
self.peer = peer
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.selectionState = selectionState
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.sideInset != rhs.sideInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.subtitle != rhs.subtitle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.selectionState != rhs.selectionState {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let label = ComponentView<Empty>()
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
private let avatarNode: AvatarNode
|
||||||
|
|
||||||
|
private var checkLayer: CheckLayer?
|
||||||
|
|
||||||
|
private var component: PeerListItemComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.containerButton = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.avatarNode = AvatarNode(font: avatarFont)
|
||||||
|
self.avatarNode.isLayerBacked = true
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
self.addSubview(self.containerButton)
|
||||||
|
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||||
|
|
||||||
|
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component, let peer = component.peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
var hasSelectionUpdated = false
|
||||||
|
if let previousComponent = self.component {
|
||||||
|
switch previousComponent.selectionState {
|
||||||
|
case .none:
|
||||||
|
if case .none = component.selectionState {
|
||||||
|
} else {
|
||||||
|
hasSelectionUpdated = true
|
||||||
|
}
|
||||||
|
case .editing:
|
||||||
|
if case .editing = component.selectionState {
|
||||||
|
} else {
|
||||||
|
hasSelectionUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let contextInset: CGFloat = 0.0
|
||||||
|
|
||||||
|
let height: CGFloat = 60.0
|
||||||
|
let verticalInset: CGFloat = 1.0
|
||||||
|
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||||
|
let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||||
|
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||||
|
|
||||||
|
if case let .editing(isSelected, isTinted) = component.selectionState {
|
||||||
|
leftInset += 44.0
|
||||||
|
avatarLeftInset += 44.0
|
||||||
|
let checkSize: CGFloat = 22.0
|
||||||
|
|
||||||
|
let checkLayer: CheckLayer
|
||||||
|
if let current = self.checkLayer {
|
||||||
|
checkLayer = current
|
||||||
|
if themeUpdated {
|
||||||
|
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
if isTinted {
|
||||||
|
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35)
|
||||||
|
}
|
||||||
|
checkLayer.theme = theme
|
||||||
|
}
|
||||||
|
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||||
|
} else {
|
||||||
|
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||||
|
if isTinted {
|
||||||
|
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35)
|
||||||
|
}
|
||||||
|
checkLayer = CheckLayer(theme: theme)
|
||||||
|
self.checkLayer = checkLayer
|
||||||
|
self.containerButton.layer.addSublayer(checkLayer)
|
||||||
|
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||||
|
checkLayer.setSelected(isSelected, animated: false)
|
||||||
|
checkLayer.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||||
|
} else {
|
||||||
|
if let checkLayer = self.checkLayer {
|
||||||
|
self.checkLayer = nil
|
||||||
|
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||||
|
checkLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarSize: CGFloat = 40.0
|
||||||
|
|
||||||
|
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
if self.avatarNode.bounds.isEmpty {
|
||||||
|
self.avatarNode.frame = avatarFrame
|
||||||
|
} else {
|
||||||
|
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||||
|
}
|
||||||
|
if let peer = component.peer {
|
||||||
|
let clipStyle: AvatarNodeClipStyle
|
||||||
|
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||||
|
clipStyle = .roundedRect
|
||||||
|
} else {
|
||||||
|
clipStyle = .round
|
||||||
|
}
|
||||||
|
if peer.id == component.context.account.peerId {
|
||||||
|
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
} else {
|
||||||
|
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let labelData: (String, Bool)
|
||||||
|
if let subtitle = component.subtitle {
|
||||||
|
labelData = (subtitle, false)
|
||||||
|
} else if case .legacyGroup = component.peer {
|
||||||
|
labelData = ("group", false)
|
||||||
|
} else if case let .channel(channel) = component.peer {
|
||||||
|
if case .group = channel.info {
|
||||||
|
labelData = ("group", false)
|
||||||
|
} else {
|
||||||
|
labelData = ("channel", false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelData = ("group", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelSize = self.label.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let previousTitleFrame = self.title.view?.frame
|
||||||
|
var previousTitleContents: UIView?
|
||||||
|
if hasSelectionUpdated && !"".isEmpty {
|
||||||
|
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat = 1.0
|
||||||
|
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(titleView)
|
||||||
|
}
|
||||||
|
titleView.frame = titleFrame
|
||||||
|
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||||
|
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||||
|
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||||
|
self.addSubview(previousTitleContents)
|
||||||
|
|
||||||
|
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||||
|
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||||
|
previousTitleContents?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let labelView = self.label.view {
|
||||||
|
if labelView.superview == nil {
|
||||||
|
labelView.isUserInteractionEnabled = false
|
||||||
|
self.containerButton.addSubview(labelView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
self.separatorLayer.isHidden = !component.hasNext
|
||||||
|
|
||||||
|
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||||
|
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
@ -1021,6 +1021,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
} else {
|
} else {
|
||||||
strongSelf.controllerInteraction.presentController(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
strongSelf.controllerInteraction.presentController(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
||||||
}
|
}
|
||||||
|
case .chatFolder:
|
||||||
|
break
|
||||||
case let .instantView(webpage, anchor):
|
case let .instantView(webpage, anchor):
|
||||||
strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor))
|
strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor))
|
||||||
case let .join(link):
|
case let .join(link):
|
||||||
|
@ -29,6 +29,7 @@ import WebUI
|
|||||||
import BotPaymentsUI
|
import BotPaymentsUI
|
||||||
import PremiumUI
|
import PremiumUI
|
||||||
import AuthorizationUI
|
import AuthorizationUI
|
||||||
|
import ChatFolderLinkPreviewScreen
|
||||||
|
|
||||||
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
|
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
|
||||||
if case .default = navigation {
|
if case .default = navigation {
|
||||||
@ -750,5 +751,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
|||||||
} else {
|
} else {
|
||||||
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Chat_ErrorInvoiceNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||||
}
|
}
|
||||||
|
case let .chatFolder(slug):
|
||||||
|
if let navigationController = navigationController {
|
||||||
|
navigationController.pushViewController(ChatFolderLinkPreviewScreen(context: context, slug: slug))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -837,6 +837,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleResolvedUrl(.premiumOffer(reference: reference))
|
handleResolvedUrl(.premiumOffer(reference: reference))
|
||||||
|
} else if parsedUrl.host == "folder" {
|
||||||
|
if let components = URLComponents(string: "/?" + query) {
|
||||||
|
var slug: String?
|
||||||
|
if let queryItems = components.queryItems {
|
||||||
|
for queryItem in queryItems {
|
||||||
|
if let value = queryItem.value {
|
||||||
|
if queryItem.name == "slug" {
|
||||||
|
slug = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let slug = slug {
|
||||||
|
convertedUrl = "https://t.me/folder/\(slug)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if parsedUrl.host == "importStickers" {
|
if parsedUrl.host == "importStickers" {
|
||||||
|
@ -97,6 +97,7 @@ public enum ParsedInternalUrl {
|
|||||||
case phone(String, String?, String?)
|
case phone(String, String?, String?)
|
||||||
case startAttach(String, String?, String?)
|
case startAttach(String, String?, String?)
|
||||||
case contactToken(String)
|
case contactToken(String)
|
||||||
|
case chatFolder(slug: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ParsedUrl {
|
private enum ParsedUrl {
|
||||||
@ -417,6 +418,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
|
|||||||
return .wallpaper(parameter)
|
return .wallpaper(parameter)
|
||||||
} else if pathComponents[0] == "addtheme" {
|
} else if pathComponents[0] == "addtheme" {
|
||||||
return .theme(pathComponents[1])
|
return .theme(pathComponents[1])
|
||||||
|
} else if pathComponents[0] == "folder" {
|
||||||
|
return .chatFolder(slug: pathComponents[1])
|
||||||
} else if pathComponents.count == 3 && pathComponents[0] == "c" {
|
} else if pathComponents.count == 3 && pathComponents[0] == "c" {
|
||||||
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) {
|
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) {
|
||||||
var threadId: Int32?
|
var threadId: Int32?
|
||||||
@ -774,6 +777,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
|||||||
}
|
}
|
||||||
case let .stickerPack(name, type):
|
case let .stickerPack(name, type):
|
||||||
return .single(.stickerPack(name: name, type: type))
|
return .single(.stickerPack(name: name, type: type))
|
||||||
|
case let .chatFolder(slug):
|
||||||
|
return .single(.chatFolder(slug: slug))
|
||||||
case let .invoice(slug):
|
case let .invoice(slug):
|
||||||
return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug))
|
return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug))
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user