[WIP] Folders

This commit is contained in:
Ali 2023-03-21 23:38:48 +04:00
parent cd70df2448
commit c0dbb68a3c
23 changed files with 1817 additions and 97 deletions

View File

@ -298,6 +298,7 @@ public enum ResolvedUrl {
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
case invoice(slug: String, invoice: TelegramMediaInvoice?)
case premiumOffer(reference: String?)
case chatFolder(slug: String)
}
public enum NavigateToChatKeepStack {

View File

@ -43,6 +43,7 @@ import PeerInfoUI
import ComponentDisplayAdapters
import ChatListHeaderComponent
import ChatListTitleView
import InviteLinksUI
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
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
}
}
@ -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) {
let actionSheet = ActionSheetController(presentationData: self.presentationData)

View File

@ -188,7 +188,7 @@ private enum ChatListFilterPresetEntrySortId: Comparable {
case let .inviteLink(rhsIndex):
return lhsIndex < rhsIndex
case .inviteLinkFooter:
return false
return true
}
case .inviteLinkFooter:
switch rhs {
@ -1059,8 +1059,8 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
if let currentPreset {
sharedLinks.set(context.engine.peers.getExportedChatLinks(id: currentPreset.id)
|> map(Optional.init))
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id)
|> map(Optional.init)))
}
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
@ -1266,20 +1266,43 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
},
createLink: {
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 {
} else {
let _ = (context.engine.peers.exportChatFolder(filterId: currentPreset.id, title: "Link", peerIds: data.includePeers.peers)
|> deliverOnMainQueue).start(completed: {
dismissImpl?()
if let updatedLink {
links.insert(updatedLink, at: 0)
sharedLinks.set(.single(links))
}
})
}*/
}))
}
}, openLink: { link in
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))
}
}
})
}
}))
}
}
)

View File

@ -6,16 +6,17 @@ import LegacyComponents
import TelegramPresentationData
public struct CheckNodeTheme {
public let backgroundColor: UIColor
public let strokeColor: UIColor
public let borderColor: UIColor
public let overlayBorder: Bool
public let hasInset: Bool
public let hasShadow: Bool
public let filledBorder: Bool
public let borderWidth: CGFloat?
public var backgroundColor: UIColor
public var strokeColor: UIColor
public var borderColor: UIColor
public var overlayBorder: Bool
public var hasInset: Bool
public var hasShadow: Bool
public var filledBorder: Bool
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.strokeColor = strokeColor
self.borderColor = borderColor
@ -24,6 +25,7 @@ public struct CheckNodeTheme {
self.hasShadow = hasShadow
self.filledBorder = filledBorder
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)
context.setStrokeColor(parameters.theme.borderColor.cgColor)
if parameters.theme.isDottedBorder {
context.setLineDash(phase: 0.0, lengths: [4.0, 4.0])
}
context.setLineWidth(borderWidth)
let maybeScaleOut = {

View File

@ -27,8 +27,8 @@ private final class FolderInviteLinkListControllerArguments {
let shareMainLink: (String) -> Void
let openMainLink: (String) -> Void
let copyLink: (String) -> Void
let mainLinkContextAction: (String?, ASDisplayNode, ContextGesture?) -> Void
let peerAction: (EnginePeer) -> Void
let mainLinkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void
let peerAction: (EnginePeer, Bool) -> Void
let generateLink: () -> Void
init(
@ -36,8 +36,8 @@ private final class FolderInviteLinkListControllerArguments {
shareMainLink: @escaping (String) -> Void,
openMainLink: @escaping (String) -> Void,
copyLink: @escaping (String) -> Void,
mainLinkContextAction: @escaping (String?, ASDisplayNode, ContextGesture?) -> Void,
peerAction: @escaping (EnginePeer) -> Void,
mainLinkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void,
peerAction: @escaping (EnginePeer, Bool) -> Void,
generateLink: @escaping () -> Void
) {
self.context = context
@ -68,7 +68,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool)
case peersHeader(String)
case peer(index: Int, peer: EnginePeer, isSelected: Bool)
case peer(index: Int, peer: EnginePeer, isSelected: Bool, isEnabled: Bool)
case peersInfo(String)
var section: ItemListSectionId {
@ -94,7 +94,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return .index(4)
case .peersInfo:
return .index(5)
case let .peer(_, peer, _):
case let .peer(_, peer, _, _):
return .peer(peer.id)
}
}
@ -109,7 +109,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return 2
case .peersHeader:
return 4
case let .peer(index, _, _):
case let .peer(index, _, _, _):
return 10 + index
case .peersInfo:
return 1000
@ -148,8 +148,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else {
return false
}
case let .peer(index, peer, isSelected):
if case .peer(index, peer, isSelected) = rhs {
case let .peer(index, peer, isSelected, isEnabled):
if case .peer(index, peer, isSelected, isEnabled) = rhs {
return true
} else {
return false
@ -180,7 +180,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
arguments.generateLink()
}
}, contextAction: { node, gesture in
arguments.mainLinkContextAction(link?.link, node, gesture)
arguments.mainLinkContextAction(link, node, gesture)
}, viewAction: {
if let link {
arguments.openMainLink(link.link)
@ -190,7 +190,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peersInfo(text):
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(
presentationData: presentationData,
dateTimeFormat: PresentationDateTimeFormat(),
@ -198,15 +199,15 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
context: arguments.context,
peer: peer,
presence: nil,
text: .none,
text: .text(isEnabled ? "you can invite others here" : "you can't invite others here", .secondary),
label: .none,
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck),
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled),
enabled: true,
selectable: true,
sectionId: self.section,
action: {
arguments.peerAction(peer)
arguments.peerAction(peer, isEnabled)
},
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(
presentationData: PresentationData,
state: FolderInviteLinkListControllerState,
@ -224,16 +240,38 @@ private func folderInviteLinkListControllerEntries(
) -> [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
entries.append(.mainLinkHeader("INVITE LINK"))
entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink))
entries.append(.peersHeader("\(allPeers.count) CHATS SELECTED"))
for peer in allPeers {
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id)))
entries.append(.peersHeader(peersHeaderString))
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
@ -245,11 +283,12 @@ private struct FolderInviteLinkListControllerState: Equatable {
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)?
let _ = pushControllerImpl
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var dismissTooltipsImpl: (() -> Void)?
@ -257,9 +296,6 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
var initialState = FolderInviteLinkListControllerState()
initialState.currentLink = currentInvitation
for peerId in allPeerIds {
initialState.selectedPeerIds.insert(peerId)
}
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((FolderInviteLinkListControllerState) -> FolderInviteLinkListControllerState) -> Void = { f in
@ -275,6 +311,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
var getControllerImpl: (() -> ViewController?)?
var displayTooltipImpl: ((UndoOverlayContent) -> Void)?
let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in
let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData)
shareController.completed = { peerIds in
@ -338,7 +376,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
dismissTooltipsImpl?()
UIPasteboard.general.string = invite
UIPasteboard.general.string = invite.link
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)
@ -349,12 +387,33 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
}, action: { _, f in
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)
presentInGlobalOverlayImpl?(contextController)
}, peerAction: { peer in
}, peerAction: { peer, isEnabled in
let state = stateValue.with({ $0 })
if state.currentLink != nil {
return
}
if isEnabled {
updateState { state in
var state = state
@ -366,6 +425,18 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
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: {
let currentState = stateValue.with({ $0 })
if !currentState.generatingLink {
@ -377,8 +448,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
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
linkUpdated(result)
updateState { state in
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(
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 signal = combineLatest(queue: .mainQueue(),
presentationData,
@ -410,7 +521,23 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
let title: ItemListControllerTitle
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(
presentationData: presentationData,
state: state,
@ -451,9 +578,18 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
controller.presentInGlobalOverlay(c)
}
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
getControllerImpl = { [weak controller] in
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
controller?.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {

View File

@ -519,6 +519,13 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
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 lineDiameter: CGFloat = 12.0
let titleFrame = strongSelf.invitedPeersNode.frame

View File

@ -275,8 +275,8 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte
let nextColor: ItemBackgroundColor
let transitionFraction: CGFloat
color = .green
nextColor = .yellow
color = .blue
nextColor = .blue
transitionFraction = 1.0
let topColor = color.colors.top

View File

@ -253,10 +253,12 @@ public enum ItemListPeerItemLabel {
public struct ItemListPeerItemSwitch {
public var value: Bool
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.style = style
self.isEnabled = isEnabled
}
}
@ -1251,7 +1253,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
if let current = strongSelf.leftCheckNode {
leftCheckNode = current
} 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.avatarNode.supernode?.addSubnode(leftCheckNode)
}

View File

@ -38,6 +38,7 @@ public final class QrCodeScreen: ViewController {
public enum Subject {
case peer(peer: EnginePeer)
case invite(invite: ExportedInvitation, isGroup: Bool)
case chatFolder(slug: String)
var link: String {
switch self {
@ -45,6 +46,8 @@ public final class QrCodeScreen: ViewController {
return "https://t.me/\(peer.addressName ?? "")"
case let .invite(invite, _):
return invite.link ?? ""
case let .chatFolder(slug):
return "https://t.me/folder/\(slug)"
}
}
@ -54,6 +57,8 @@ public final class QrCodeScreen: ViewController {
return "Q"
case .invite:
return "Q"
case .chatFolder:
return "Q"
}
}
}

View File

@ -238,7 +238,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[594758406] = { return Api.EncryptedMessage.parse_encryptedMessageService($0) }
dict[179611673] = { return Api.ExportedChatInvite.parse_chatInviteExported($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[1571494644] = { return Api.ExportedMessageLink.parse_exportedMessageLink($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[1805101290] = { return Api.communities.ExportedCommunityInvite.parse_exportedCommunityInvite($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[182326673] = { return Api.contacts.Blocked.parse_blocked($0) }
dict[-513392236] = { return Api.contacts.Blocked.parse_blockedSlice($0) }

View File

@ -958,15 +958,16 @@ public extension Api.communities {
}
public extension Api.community {
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])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .communityInvite(let peers, let chats, let users):
case .communityInvite(let title, let peers, let chats, let users):
if boxed {
buffer.appendInt32(408604768)
buffer.appendInt32(988463765)
}
serializeString(title, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(peers.count))
for item in peers {
@ -1009,31 +1010,34 @@ public extension Api.community {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .communityInvite(let peers, let chats, let users):
return ("communityInvite", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)])
case .communityInvite(let title, let peers, let chats, let users):
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):
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? {
var _1: [Api.Peer]?
var _1: String?
_1 = parseString(reader)
var _2: [Api.Peer]?
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() {
_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() {
_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 _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.community.CommunityInvite.communityInvite(peers: _1!, chats: _2!, users: _3!)
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.community.CommunityInvite.communityInvite(title: _1!, peers: _2!, chats: _3!, users: _4!)
}
else {
return nil

View File

@ -1026,14 +1026,15 @@ public extension Api {
}
public extension Api {
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) {
switch self {
case .exportedCommunityInvite(let title, let url, let peers):
case .exportedCommunityInvite(let flags, let title, let url, let peers):
if boxed {
buffer.appendInt32(-1350894801)
buffer.appendInt32(-337788502)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
serializeString(url, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
@ -1047,25 +1048,28 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .exportedCommunityInvite(let title, let url, let peers):
return ("exportedCommunityInvite", [("title", title as Any), ("url", url as Any), ("peers", peers as Any)])
case .exportedCommunityInvite(let flags, let title, let url, let peers):
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? {
var _1: String?
_1 = parseString(reader)
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: [Api.Peer]?
var _3: String?
_3 = parseString(reader)
var _4: [Api.Peer]?
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 _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.ExportedCommunityInvite.exportedCommunityInvite(title: _1!, url: _2!, peers: _3!)
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.ExportedCommunityInvite.exportedCommunityInvite(flags: _1!, title: _2!, url: _3!, peers: _4!)
}
else {
return nil

View File

@ -15,15 +15,28 @@ public struct ExportedChatFolderLink: Equatable {
public var title: String
public var link: String
public var peerIds: [EnginePeer.Id]
public var isRevoked: Bool
public init(
title: String,
link: String,
peerIds: [EnginePeer.Id]
peerIds: [EnginePeer.Id],
isRevoked: Bool
) {
self.title = title
self.link = link
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 {
case let .exportedCommunityInvite(title, url, peers):
case let .exportedCommunityInvite(flags, title, url, peers):
return .single(ExportedChatFolderLink(
title: title,
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)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.communities.ExportedInvites?, NoError> in
@ -105,8 +119,13 @@ func _internal_getExportedChatLinks(account: Account, id: Int32) -> Signal<[Expo
var result: [ExportedChatFolderLink] = []
for invite in invites {
switch invite {
case let .exportedCommunityInvite(title, url, peers):
result.append(ExportedChatFolderLink(title: title, link: url, peerIds: peers.map(\.peerId)))
case let .exportedCommunityInvite(flags, title, url, peers):
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()
}
}
}

View File

@ -1030,8 +1030,24 @@ public extension TelegramEngine {
return _internal_exportChatFolder(account: self.account, filterId: filterId, title: title, peerIds: peerIds)
}
public func getExportedChatLinks(id: Int32) -> Signal<[ExportedChatFolderLink], NoError> {
return _internal_getExportedChatLinks(account: self.account, id: id)
public func getExportedChatFolderLinks(id: Int32) -> Signal<[ExportedChatFolderLink], NoError> {
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)
}
}
}

View File

@ -358,6 +358,7 @@ swift_library(
"//submodules/DrawingUI:DrawingUI",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],

View File

@ -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",
],
)

View File

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

View File

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

View File

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

View File

@ -1021,6 +1021,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
} 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)
}
case .chatFolder:
break
case let .instantView(webpage, anchor):
strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor))
case let .join(link):

View File

@ -29,6 +29,7 @@ import WebUI
import BotPaymentsUI
import PremiumUI
import AuthorizationUI
import ChatFolderLinkPreviewScreen
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
if case .default = navigation {
@ -750,5 +751,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} 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)
}
case let .chatFolder(slug):
if let navigationController = navigationController {
navigationController.pushViewController(ChatFolderLinkPreviewScreen(context: context, slug: slug))
}
}
}

View File

@ -837,6 +837,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
}
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 {
if parsedUrl.host == "importStickers" {

View File

@ -97,6 +97,7 @@ public enum ParsedInternalUrl {
case phone(String, String?, String?)
case startAttach(String, String?, String?)
case contactToken(String)
case chatFolder(slug: String)
}
private enum ParsedUrl {
@ -417,6 +418,8 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? {
return .wallpaper(parameter)
} else if pathComponents[0] == "addtheme" {
return .theme(pathComponents[1])
} else if pathComponents[0] == "folder" {
return .chatFolder(slug: pathComponents[1])
} else if pathComponents.count == 3 && pathComponents[0] == "c" {
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]) {
var threadId: Int32?
@ -774,6 +777,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
}
case let .stickerPack(name, type):
return .single(.stickerPack(name: name, type: type))
case let .chatFolder(slug):
return .single(.chatFolder(slug: slug))
case let .invoice(slug):
return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug))
|> map(Optional.init)