mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Folder improvements
This commit is contained in:
parent
45e4728ae0
commit
6f04ec274f
@ -1544,7 +1544,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
//TODO:localize
|
||||
|
||||
for filter in filters {
|
||||
if filter.id == filterId, case let .filter(_, _, _, data) = filter {
|
||||
if filter.id == filterId, case let .filter(_, title, _, 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)
|
||||
@ -1553,7 +1553,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.shareFolder(filterId: filterId, data: data)
|
||||
strongSelf.shareFolder(filterId: filterId, data: data, title: title)
|
||||
})
|
||||
})))
|
||||
}
|
||||
@ -2699,9 +2699,9 @@ 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 shareFolder(filterId: Int32, data: ChatListFilterData, title: String) {
|
||||
/*self.push(folderInviteLinkListController(context: self.context, filterId: filterId, title: title, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { _ in
|
||||
}))*/
|
||||
}
|
||||
|
||||
private func askForFilterRemoval(id: Int32) {
|
||||
|
@ -317,7 +317,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
||||
case includeExpand(String)
|
||||
case excludeExpand(String)
|
||||
case inviteLinkHeader
|
||||
case inviteLinkCreate
|
||||
case inviteLinkCreate(hasLinks: Bool)
|
||||
case inviteLink(Int, ExportedChatFolderLink)
|
||||
case inviteLinkInfo
|
||||
|
||||
@ -518,10 +518,10 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
||||
})
|
||||
case .inviteLinkHeader:
|
||||
//TODO:localize
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", sectionId: self.section)
|
||||
case .inviteLinkCreate:
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section)
|
||||
case let.inviteLinkCreate(hasLinks):
|
||||
//TODO:localize
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "Share Folder with Others", sectionId: self.section, editing: false, action: {
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? "Create a New Link" : "Share Folder", sectionId: self.section, editing: false, action: {
|
||||
arguments.createLink()
|
||||
})
|
||||
case let .inviteLink(_, link):
|
||||
@ -532,7 +532,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
||||
}
|
||||
case .inviteLinkInfo:
|
||||
//TODO:localize
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown("Give vour friends and colleagues access to the entire folder including all of its groups and channels where you have the necessary rights."), sectionId: self.section)
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown("Share access to some of this folder's groups and channels with others."), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -575,7 +575,7 @@ private struct ChatListFilterPresetControllerState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?) -> [ChatListFilterPresetEntry] {
|
||||
private func chatListFilterPresetControllerEntries(presentationData: PresentationData, isNewFilter: Bool, currentPreset: ChatListFilter?, state: ChatListFilterPresetControllerState, includePeers: [EngineRenderedPeer], excludePeers: [EngineRenderedPeer], isPremium: Bool, limit: Int32, inviteLinks: [ExportedChatFolderLink]?) -> [ChatListFilterPresetEntry] {
|
||||
var entries: [ChatListFilterPresetEntry] = []
|
||||
|
||||
if isNewFilter {
|
||||
@ -614,46 +614,49 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
|
||||
|
||||
entries.append(.includePeerInfo(presentationData.strings.ChatListFolder_IncludeSectionInfo))
|
||||
|
||||
entries.append(.excludePeersHeader(presentationData.strings.ChatListFolder_ExcludedSectionHeader))
|
||||
entries.append(.addExcludePeer(title: presentationData.strings.ChatListFolder_AddChats))
|
||||
|
||||
var excludeCategoryIndex = 0
|
||||
for category in ChatListFilterExcludeCategory.allCases {
|
||||
let isExcluded: Bool
|
||||
switch category {
|
||||
case .read:
|
||||
isExcluded = state.excludeRead
|
||||
case .muted:
|
||||
isExcluded = state.excludeMuted
|
||||
case .archived:
|
||||
isExcluded = state.excludeArchived
|
||||
if let currentPreset, let data = currentPreset.data, data.isShared {
|
||||
} else {
|
||||
entries.append(.excludePeersHeader(presentationData.strings.ChatListFolder_ExcludedSectionHeader))
|
||||
entries.append(.addExcludePeer(title: presentationData.strings.ChatListFolder_AddChats))
|
||||
|
||||
var excludeCategoryIndex = 0
|
||||
for category in ChatListFilterExcludeCategory.allCases {
|
||||
let isExcluded: Bool
|
||||
switch category {
|
||||
case .read:
|
||||
isExcluded = state.excludeRead
|
||||
case .muted:
|
||||
isExcluded = state.excludeMuted
|
||||
case .archived:
|
||||
isExcluded = state.excludeArchived
|
||||
}
|
||||
|
||||
if isExcluded {
|
||||
entries.append(.excludeCategory(index: excludeCategoryIndex, category: category, title: category.title(strings: presentationData.strings), isRevealed: state.revealedItemId == .excludeCategory(category)))
|
||||
}
|
||||
excludeCategoryIndex += 1
|
||||
}
|
||||
|
||||
if isExcluded {
|
||||
entries.append(.excludeCategory(index: excludeCategoryIndex, category: category, title: category.title(strings: presentationData.strings), isRevealed: state.revealedItemId == .excludeCategory(category)))
|
||||
}
|
||||
excludeCategoryIndex += 1
|
||||
}
|
||||
|
||||
if !excludePeers.isEmpty {
|
||||
var count = 0
|
||||
for peer in excludePeers {
|
||||
entries.append(.excludePeer(index: entries.count, peer: peer, isRevealed: state.revealedItemId == .peer(peer.peerId)))
|
||||
count += 1
|
||||
if excludePeers.count >= 7 && count == 5 && !state.expandedSections.contains(.exclude) {
|
||||
break
|
||||
if !excludePeers.isEmpty {
|
||||
var count = 0
|
||||
for peer in excludePeers {
|
||||
entries.append(.excludePeer(index: entries.count, peer: peer, isRevealed: state.revealedItemId == .peer(peer.peerId)))
|
||||
count += 1
|
||||
if excludePeers.count >= 7 && count == 5 && !state.expandedSections.contains(.exclude) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if count < excludePeers.count {
|
||||
entries.append(.excludeExpand(presentationData.strings.ChatListFilter_ShowMoreChats(Int32(excludePeers.count - count))))
|
||||
}
|
||||
}
|
||||
if count < excludePeers.count {
|
||||
entries.append(.excludeExpand(presentationData.strings.ChatListFilter_ShowMoreChats(Int32(excludePeers.count - count))))
|
||||
}
|
||||
|
||||
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
|
||||
}
|
||||
|
||||
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
|
||||
|
||||
if !isNewFilter, let inviteLinks {
|
||||
entries.append(.inviteLinkHeader)
|
||||
entries.append(.inviteLinkCreate)
|
||||
entries.append(.inviteLinkCreate(hasLinks: !inviteLinks.isEmpty))
|
||||
|
||||
var index = 0
|
||||
for link in inviteLinks {
|
||||
@ -691,38 +694,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.contacts.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
|
||||
title: presentationData.strings.ChatListFolder_CategoryContacts
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.nonContacts.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
|
||||
title: presentationData.strings.ChatListFolder_CategoryNonContacts
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.groups.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), cornerRadius: 12.0, color: .green),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .green),
|
||||
title: presentationData.strings.ChatListFolder_CategoryGroups
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.channels.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: 12.0, color: .red),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .red),
|
||||
title: presentationData.strings.ChatListFolder_CategoryChannels
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.bots.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), cornerRadius: 12.0, color: .violet),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .violet),
|
||||
title: presentationData.strings.ChatListFolder_CategoryBots
|
||||
)
|
||||
]
|
||||
var additionalCategories: [ChatListNodeAdditionalCategory] = []
|
||||
var selectedCategories = Set<Int>()
|
||||
let categoryMapping: [ChatListFilterPeerCategories: AdditionalCategoryId] = [
|
||||
.contacts: .contacts,
|
||||
@ -731,9 +703,46 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
.channels: .channels,
|
||||
.bots: .bots
|
||||
]
|
||||
for (category, id) in categoryMapping {
|
||||
if filterData.categories.contains(category) {
|
||||
selectedCategories.insert(id.rawValue)
|
||||
|
||||
if let data = filter.data, data.isShared {
|
||||
} else {
|
||||
additionalCategories = [
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.contacts.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
|
||||
title: presentationData.strings.ChatListFolder_CategoryContacts
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.nonContacts.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
|
||||
title: presentationData.strings.ChatListFolder_CategoryNonContacts
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.groups.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), cornerRadius: 12.0, color: .green),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .green),
|
||||
title: presentationData.strings.ChatListFolder_CategoryGroups
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.channels.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: 12.0, color: .red),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .red),
|
||||
title: presentationData.strings.ChatListFolder_CategoryChannels
|
||||
),
|
||||
ChatListNodeAdditionalCategory(
|
||||
id: AdditionalCategoryId.bots.rawValue,
|
||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), cornerRadius: 12.0, color: .violet),
|
||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .violet),
|
||||
title: presentationData.strings.ChatListFolder_CategoryBots
|
||||
)
|
||||
]
|
||||
|
||||
for (category, id) in categoryMapping {
|
||||
if filterData.categories.contains(category) {
|
||||
selectedCategories.insert(id.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1059,8 +1068,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
||||
|
||||
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
|
||||
if let currentPreset {
|
||||
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id)
|
||||
|> map(Optional.init)))
|
||||
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: currentPreset.id)))
|
||||
}
|
||||
|
||||
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
|
||||
@ -1265,23 +1273,80 @@ 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, linkUpdated: { updatedLink in
|
||||
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
|
||||
guard var links else {
|
||||
return
|
||||
}
|
||||
|
||||
if let updatedLink {
|
||||
links.insert(updatedLink, at: 0)
|
||||
sharedLinks.set(.single(links))
|
||||
}
|
||||
})
|
||||
}))
|
||||
let state = stateValue.with({ $0 })
|
||||
|
||||
if let currentPreset, !state.additionallyIncludePeers.isEmpty {
|
||||
let _ = (context.engine.data.get(
|
||||
EngineDataList(state.additionallyIncludePeers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { peers in
|
||||
let peers = peers.compactMap({ $0 })
|
||||
if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) {
|
||||
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: nil, linkUpdated: { updatedLink in
|
||||
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
|
||||
guard var links else {
|
||||
return
|
||||
}
|
||||
|
||||
if let updatedLink {
|
||||
links.insert(updatedLink, at: 0)
|
||||
sharedLinks.set(.single(links))
|
||||
}
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
let _ = (context.engine.peers.exportChatFolder(filterId: currentPreset.id, title: "", peerIds: state.additionallyIncludePeers)
|
||||
|> deliverOnMainQueue).start(next: { link in
|
||||
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
|
||||
guard var links else {
|
||||
return
|
||||
}
|
||||
|
||||
links.insert(link, at: 0)
|
||||
sharedLinks.set(.single(links))
|
||||
})
|
||||
|
||||
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, 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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
}, error: { error in
|
||||
//TODO:localize
|
||||
let text: String
|
||||
switch error {
|
||||
case .generic:
|
||||
text = "An error occurred"
|
||||
case .limitExceeded:
|
||||
text = "You can't create more links."
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, openLink: { link in
|
||||
if let currentPreset, let data = currentPreset.data {
|
||||
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, allPeerIds: data.includePeers.peers, currentInvitation: link, linkUpdated: { updatedLink in
|
||||
if let currentPreset, let _ = currentPreset.data {
|
||||
let state = stateValue.with({ $0 })
|
||||
pushControllerImpl?(folderInviteLinkListController(context: context, filterId: currentPreset.id, title: currentPreset.title, allPeerIds: state.additionallyIncludePeers, currentInvitation: link, linkUpdated: { updatedLink in
|
||||
if updatedLink != link {
|
||||
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
|
||||
guard var links else {
|
||||
@ -1387,7 +1452,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(currentPreset != nil ? presentationData.strings.ChatListFolder_TitleEdit : presentationData.strings.ChatListFolder_TitleCreate), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, state: state, includePeers: includePeers, excludePeers: excludePeers, isPremium: isPremium, limit: premiumLimits.maxFolderChatsCount, inviteLinks: sharedLinks), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetControllerEntries(presentationData: presentationData, isNewFilter: currentPreset == nil, currentPreset: currentPreset, state: state, includePeers: includePeers, excludePeers: excludePeers, isPremium: isPremium, limit: premiumLimits.maxFolderChatsCount, inviteLinks: sharedLinks), style: .blocks, emptyStateItem: nil, animateChanges: !skipStateAnimation)
|
||||
skipStateAnimation = false
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
|
@ -57,6 +57,7 @@ swift_library(
|
||||
"//submodules/LocalizedPeerData:LocalizedPeerData",
|
||||
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
"//submodules/PromptUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,6 +21,7 @@ import ItemListPeerItem
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import QrCodeUI
|
||||
import PromptUI
|
||||
|
||||
private final class FolderInviteLinkListControllerArguments {
|
||||
let context: AccountContext
|
||||
@ -169,16 +170,20 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
case let .mainLinkHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .mainLink(link, isGenerating):
|
||||
return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: link != nil ? "Share Invite Link" : "Generate Invite Link", displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
|
||||
return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: link != nil ? "Copy" : "Generate Invite Link", secondaryButtonTitle: link != nil ? "Share" : nil, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
|
||||
if let link {
|
||||
arguments.copyLink(link.link)
|
||||
}
|
||||
}, shareAction: {
|
||||
if let link {
|
||||
arguments.shareMainLink(link.link)
|
||||
arguments.copyLink(link.link)
|
||||
} else {
|
||||
arguments.generateLink()
|
||||
}
|
||||
}, secondaryAction: {
|
||||
if let link {
|
||||
arguments.shareMainLink(link.link)
|
||||
}
|
||||
}, contextAction: { node, gesture in
|
||||
arguments.mainLinkContextAction(link, node, gesture)
|
||||
}, viewAction: {
|
||||
@ -205,6 +210,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled),
|
||||
enabled: true,
|
||||
selectable: true,
|
||||
highlightable: false,
|
||||
sectionId: self.section,
|
||||
action: {
|
||||
arguments.peerAction(peer, isEnabled)
|
||||
@ -218,46 +224,44 @@ 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,
|
||||
title: String,
|
||||
allPeers: [EnginePeer]
|
||||
) -> [InviteLinksListEntry] {
|
||||
var entries: [InviteLinksListEntry] = []
|
||||
|
||||
//TODO:localize
|
||||
|
||||
var infoString: String?
|
||||
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."
|
||||
|
||||
let canShareChats = !allPeers.allSatisfy({ !canShareLinkToPeer(peer: $0) })
|
||||
|
||||
if !canShareChats {
|
||||
infoString = "You can only share groups and channels in which you are allowed to create invite links."
|
||||
chatCountString = "There are no chats in this folder that you can share with others."
|
||||
peersHeaderString = "THESE CHATS CANNOT BE SHARED"
|
||||
} else if state.selectedPeerIds.isEmpty {
|
||||
chatCountString = "Anyone with this link can add \(title) 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."
|
||||
chatCountString = "Anyone with this link can add \(title) 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."
|
||||
chatCountString = "Anyone with this link can add \(title) 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))
|
||||
if canShareChats {
|
||||
entries.append(.mainLinkHeader("INVITE LINK"))
|
||||
entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink))
|
||||
}
|
||||
|
||||
entries.append(.peersHeader(peersHeaderString))
|
||||
|
||||
@ -274,16 +278,22 @@ private func folderInviteLinkListControllerEntries(
|
||||
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled))
|
||||
}
|
||||
|
||||
if let infoString {
|
||||
entries.append(.peersInfo(infoString))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private struct FolderInviteLinkListControllerState: Equatable {
|
||||
var title: String?
|
||||
var currentLink: ExportedChatFolderLink?
|
||||
var selectedPeerIds = Set<EnginePeer.Id>()
|
||||
var generatingLink: Bool = false
|
||||
var isSaving: Bool = false
|
||||
}
|
||||
|
||||
public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filterId: Int32, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) -> ViewController {
|
||||
public func folderInviteLinkListController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filterId: Int32, title filterTitle: String, allPeerIds: [PeerId], currentInvitation: ExportedChatFolderLink?, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) -> ViewController {
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
let _ = pushControllerImpl
|
||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||
@ -295,6 +305,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
var initialState = FolderInviteLinkListControllerState()
|
||||
initialState.title = currentInvitation?.title
|
||||
initialState.currentLink = currentInvitation
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
@ -313,6 +324,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
|
||||
var displayTooltipImpl: ((UndoOverlayContent) -> Void)?
|
||||
|
||||
var didDisplayAddPeerNotice: Bool = false
|
||||
|
||||
let arguments = FolderInviteLinkListControllerArguments(context: context, shareMainLink: { inviteLink in
|
||||
let shareController = ShareController(context: context, subject: .url(inviteLink), updatedPresentationData: updatedPresentationData)
|
||||
shareController.completed = { peerIds in
|
||||
@ -368,6 +381,33 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Name Link", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let state = stateValue.with({ $0 })
|
||||
|
||||
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Link title", value: state.title ?? "", apply: { value in
|
||||
if let value {
|
||||
updateState { state in
|
||||
var state = state
|
||||
|
||||
state.title = value
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
})
|
||||
/*promptController.dismissed = { byOutsideTap in
|
||||
if byOutsideTap {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}*/
|
||||
presentControllerImpl?(promptController, nil)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
|
||||
@ -395,7 +435,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, revoke: true)
|
||||
let _ = (context.engine.peers.editChatFolderLink(filterId: filterId, link: invite, title: nil, peerIds: nil, revoke: true)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
let _ = (context.engine.peers.revokeChatFolderLink(filterId: filterId, link: invite)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
@ -408,12 +448,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
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, isEnabled in
|
||||
let state = stateValue.with({ $0 })
|
||||
if state.currentLink != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isEnabled {
|
||||
var added = false
|
||||
updateState { state in
|
||||
var state = state
|
||||
|
||||
@ -421,13 +457,22 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
state.selectedPeerIds.remove(peer.id)
|
||||
} else {
|
||||
state.selectedPeerIds.insert(peer.id)
|
||||
added = true
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
if added && !didDisplayAddPeerNotice {
|
||||
didDisplayAddPeerNotice = true
|
||||
|
||||
dismissTooltipsImpl?()
|
||||
//TODO:localize
|
||||
displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats."))
|
||||
}
|
||||
} else {
|
||||
//TODO:localized
|
||||
var text = "You can't invite others here"
|
||||
//TODO:localize
|
||||
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."
|
||||
@ -519,28 +564,57 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
|
||||
//TODO:localize
|
||||
let title: ItemListControllerTitle
|
||||
title = .text("Share Folder")
|
||||
|
||||
var folderTitle = "Share Folder"
|
||||
if let title = state.title, !title.isEmpty {
|
||||
folderTitle = title
|
||||
}
|
||||
title = .text(folderTitle)
|
||||
|
||||
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
|
||||
if state.isSaving {
|
||||
doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
|
||||
} else {
|
||||
doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
|
||||
let state = stateValue.with({ $0 })
|
||||
if let currentLink = state.currentLink {
|
||||
if currentLink.title != state.title || Set(currentLink.peerIds) != state.selectedPeerIds {
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.isSaving = true
|
||||
return state
|
||||
}
|
||||
actionsDisposable.add((context.engine.peers.editChatFolderLink(filterId: filterId, link: currentLink, title: state.title, peerIds: Array(state.selectedPeerIds), revoke: false)
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.isSaving = false
|
||||
return state
|
||||
}
|
||||
|
||||
dismissTooltipsImpl?()
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
//TODO:localize
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}, completed: {
|
||||
linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false))
|
||||
dismissImpl?()
|
||||
}))
|
||||
} else {
|
||||
dismissImpl?()
|
||||
}
|
||||
} else {
|
||||
dismissImpl?()
|
||||
}
|
||||
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,
|
||||
title: filterTitle,
|
||||
allPeers: allPeers.compactMap { $0 }
|
||||
), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: animateChanges)
|
||||
|
||||
|
@ -34,12 +34,14 @@ public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem {
|
||||
let displayButton: Bool
|
||||
let enableButton: Bool
|
||||
let buttonTitle: String
|
||||
let secondaryButtonTitle: String?
|
||||
let displayImporters: Bool
|
||||
let buttonColor: UIColor?
|
||||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let copyAction: (() -> Void)?
|
||||
let shareAction: (() -> Void)?
|
||||
let secondaryAction: (() -> Void)?
|
||||
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
let viewAction: (() -> Void)?
|
||||
public let tag: ItemListItemTag?
|
||||
@ -53,12 +55,14 @@ public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem {
|
||||
displayButton: Bool,
|
||||
enableButton: Bool,
|
||||
buttonTitle: String,
|
||||
secondaryButtonTitle: String?,
|
||||
displayImporters: Bool,
|
||||
buttonColor: UIColor?,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle,
|
||||
copyAction: (() -> Void)?,
|
||||
shareAction: (() -> Void)?,
|
||||
secondaryAction: (() -> Void)?,
|
||||
contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?,
|
||||
viewAction: (() -> Void)?,
|
||||
tag: ItemListItemTag? = nil
|
||||
@ -71,12 +75,14 @@ public class ItemListFolderInviteLinkItem: ListViewItem, ItemListItem {
|
||||
self.displayButton = displayButton
|
||||
self.enableButton = enableButton
|
||||
self.buttonTitle = buttonTitle
|
||||
self.secondaryButtonTitle = secondaryButtonTitle
|
||||
self.displayImporters = displayImporters
|
||||
self.buttonColor = buttonColor
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.copyAction = copyAction
|
||||
self.shareAction = shareAction
|
||||
self.secondaryAction = secondaryAction
|
||||
self.contextAction = contextAction
|
||||
self.viewAction = viewAction
|
||||
self.tag = tag
|
||||
@ -133,6 +139,7 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
|
||||
private let addressButtonIconNode: ASImageNode
|
||||
private var addressShimmerNode: ShimmerEffectNode?
|
||||
private var shareButtonNode: SolidRoundedButtonNode?
|
||||
private var secondaryButtonNode: SolidRoundedButtonNode?
|
||||
|
||||
private let avatarsButtonNode: HighlightTrackingButtonNode
|
||||
private let avatarsContext: AnimatedAvatarSetContext
|
||||
@ -465,13 +472,49 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod
|
||||
strongSelf.addSubnode(shareButtonNode)
|
||||
strongSelf.shareButtonNode = shareButtonNode
|
||||
}
|
||||
|
||||
shareButtonNode.title = item.buttonTitle
|
||||
|
||||
let buttonWidth = contentSize.width - leftInset - rightInset
|
||||
if let secondaryButtonTitle = item.secondaryButtonTitle {
|
||||
let secondaryButtonNode: SolidRoundedButtonNode
|
||||
if let current = strongSelf.secondaryButtonNode {
|
||||
secondaryButtonNode = current
|
||||
} else {
|
||||
let buttonTheme: SolidRoundedButtonTheme
|
||||
if let buttonColor = item.buttonColor {
|
||||
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
} else {
|
||||
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
|
||||
}
|
||||
secondaryButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 11.0)
|
||||
secondaryButtonNode.pressed = { [weak self] in
|
||||
self?.item?.secondaryAction?()
|
||||
}
|
||||
strongSelf.addSubnode(secondaryButtonNode)
|
||||
strongSelf.secondaryButtonNode = secondaryButtonNode
|
||||
}
|
||||
secondaryButtonNode.title = secondaryButtonTitle
|
||||
} else {
|
||||
if let secondaryButtonNode = strongSelf.secondaryButtonNode {
|
||||
strongSelf.secondaryButtonNode = nil
|
||||
secondaryButtonNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
var buttonWidth = contentSize.width - leftInset - rightInset
|
||||
let totalButtonWidth = buttonWidth
|
||||
let buttonSpacing: CGFloat = 8.0
|
||||
if strongSelf.secondaryButtonNode != nil {
|
||||
buttonWidth = floor((buttonWidth - 8.0) / 2.0)
|
||||
}
|
||||
|
||||
let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
|
||||
shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
|
||||
|
||||
if let secondaryButtonNode = strongSelf.secondaryButtonNode {
|
||||
let _ = secondaryButtonNode.updateLayout(width: totalButtonWidth - buttonWidth - buttonSpacing, transition: .immediate)
|
||||
secondaryButtonNode.frame = CGRect(x: leftInset + buttonWidth + buttonSpacing, y: verticalInset + fieldHeight + fieldSpacing, width: totalButtonWidth - buttonWidth - buttonSpacing, height: buttonHeight)
|
||||
}
|
||||
|
||||
var totalWidth = invitedPeersLayout.size.width
|
||||
var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0)
|
||||
let avatarSpacing: CGFloat = 21.0
|
||||
|
@ -338,6 +338,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
||||
let enabled: Bool
|
||||
let highlighted: Bool
|
||||
public let selectable: Bool
|
||||
let highlightable: Bool
|
||||
let animateFirstAvatarTransition: Bool
|
||||
public let sectionId: ItemListSectionId
|
||||
let action: (() -> Void)?
|
||||
@ -355,7 +356,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
||||
let displayDecorations: Bool
|
||||
let disableInteractiveTransitionIfNecessary: Bool
|
||||
|
||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
|
||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: EnginePeer, threadInfo: EngineMessageHistoryThread.Info? = nil, height: ItemListPeerItemHeight = .peerList, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, nameStyle: ItemListPeerItemNameStyle = .distinctBold, presence: EnginePeer.Presence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, highlighted: Bool = false, selectable: Bool, highlightable: Bool = true, animateFirstAvatarTransition: Bool = true, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true, noInsets: Bool = false, noCorners: Bool = false, tag: ItemListItemTag? = nil, header: ListViewItemHeader? = nil, shimmering: ItemListPeerItemShimmering? = nil, displayDecorations: Bool = true, disableInteractiveTransitionIfNecessary: Bool = false) {
|
||||
self.presentationData = presentationData
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
@ -375,6 +376,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
||||
self.enabled = enabled
|
||||
self.highlighted = highlighted
|
||||
self.selectable = selectable
|
||||
self.highlightable = highlightable
|
||||
self.animateFirstAvatarTransition = animateFirstAvatarTransition
|
||||
self.sectionId = sectionId
|
||||
self.action = action
|
||||
@ -1256,6 +1258,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
var checkTheme = CheckNodeTheme(theme: item.presentationData.theme, style: .plain)
|
||||
checkTheme.isDottedBorder = !switchValue.isEnabled
|
||||
leftCheckNode = CheckNode(theme: checkTheme)
|
||||
leftCheckNode.isUserInteractionEnabled = false
|
||||
strongSelf.leftCheckNode = leftCheckNode
|
||||
strongSelf.avatarNode.supernode?.addSubnode(leftCheckNode)
|
||||
}
|
||||
@ -1372,7 +1375,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.isHidden = !item.displayDecorations
|
||||
strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations
|
||||
strongSelf.highlightedBackgroundNode.isHidden = !item.displayDecorations || !item.highlightable
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
|
@ -41,6 +41,7 @@ public enum ItemListSectionHeaderActivityIndicator {
|
||||
public class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let text: String
|
||||
let badge: String?
|
||||
let multiline: Bool
|
||||
let activityIndicator: ItemListSectionHeaderActivityIndicator
|
||||
let accessoryText: ItemListSectionHeaderAccessoryText?
|
||||
@ -48,9 +49,10 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem {
|
||||
|
||||
public let isAlwaysPlain: Bool = true
|
||||
|
||||
public init(presentationData: ItemListPresentationData, text: String, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) {
|
||||
public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, sectionId: ItemListSectionId) {
|
||||
self.presentationData = presentationData
|
||||
self.text = text
|
||||
self.badge = badge
|
||||
self.multiline = multiline
|
||||
self.activityIndicator = activityIndicator
|
||||
self.accessoryText = accessoryText
|
||||
@ -97,6 +99,8 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode {
|
||||
private var item: ItemListSectionHeaderItem?
|
||||
|
||||
private let titleNode: TextNode
|
||||
private var badgeBackgroundLayer: SimpleLayer?
|
||||
private var badgeTextNode: TextNode?
|
||||
private let accessoryTextNode: TextNode
|
||||
private var accessoryImageNode: ASImageNode?
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
@ -126,6 +130,7 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode {
|
||||
|
||||
public func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode)
|
||||
let makeAccessoryTextLayout = TextNode.asyncLayout(self.accessoryTextNode)
|
||||
|
||||
let previousItem = self.item
|
||||
@ -135,7 +140,19 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode {
|
||||
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
var badgeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
if let badge = item.badge {
|
||||
let badgeFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize * 11.0 / 13.0)
|
||||
badgeLayoutAndApply = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: badge, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
}
|
||||
|
||||
let badgeSpacing: CGFloat = 6.0
|
||||
var textRightInset: CGFloat = 20.0
|
||||
if let badgeLayoutAndApply {
|
||||
textRightInset += badgeLayoutAndApply.0.size.width + badgeSpacing
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: item.multiline ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
var accessoryTextString: NSAttributedString?
|
||||
var accessoryIcon: UIImage?
|
||||
if let accessoryText = item.accessoryText {
|
||||
@ -178,6 +195,43 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode {
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size)
|
||||
|
||||
if let badgeLayoutAndApply {
|
||||
let badgeTextNode = badgeLayoutAndApply.1()
|
||||
let badgeSideInset: CGFloat = 4.0
|
||||
let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + badgeLayoutAndApply.0.size.width, height: badgeLayoutAndApply.0.size.height + 3.0)
|
||||
let badgeBackgroundFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + badgeSpacing, y: strongSelf.titleNode.frame.minY - UIScreenPixel + floorToScreenPixels((strongSelf.titleNode.bounds.height - badgeBackgroundSize.height) * 0.5)), size: badgeBackgroundSize)
|
||||
|
||||
let badgeBackgroundLayer: SimpleLayer
|
||||
if let current = strongSelf.badgeBackgroundLayer {
|
||||
badgeBackgroundLayer = current
|
||||
} else {
|
||||
badgeBackgroundLayer = SimpleLayer()
|
||||
strongSelf.badgeBackgroundLayer = badgeBackgroundLayer
|
||||
strongSelf.layer.addSublayer(badgeBackgroundLayer)
|
||||
}
|
||||
|
||||
if strongSelf.badgeTextNode !== badgeTextNode {
|
||||
strongSelf.badgeTextNode?.removeFromSupernode()
|
||||
strongSelf.badgeTextNode = badgeTextNode
|
||||
strongSelf.addSubnode(badgeTextNode)
|
||||
}
|
||||
|
||||
badgeBackgroundLayer.frame = badgeBackgroundFrame
|
||||
badgeBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemCheckColors.fillColor.cgColor
|
||||
badgeBackgroundLayer.cornerRadius = 5.0
|
||||
|
||||
badgeTextNode.frame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.minX + floor((badgeBackgroundFrame.width - badgeLayoutAndApply.0.size.width) * 0.5), y: badgeBackgroundFrame.minY + 1.0 + floorToScreenPixels((badgeBackgroundFrame.height - badgeLayoutAndApply.0.size.height) * 0.5)), size: badgeLayoutAndApply.0.size)
|
||||
} else {
|
||||
if let badgeTextNode = strongSelf.badgeTextNode {
|
||||
strongSelf.badgeTextNode = nil
|
||||
badgeTextNode.removeFromSupernode()
|
||||
}
|
||||
if let badgeBackgroundLayer = strongSelf.badgeBackgroundLayer {
|
||||
strongSelf.badgeBackgroundLayer = nil
|
||||
badgeBackgroundLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
var accessoryTextOffset: CGFloat = 0.0
|
||||
if let accessoryIcon = accessoryIcon {
|
||||
accessoryTextOffset += accessoryIcon.size.width + 3.0
|
||||
|
@ -3,12 +3,24 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
|
||||
//communities.exportCommunityInvite#41fe69d9 community:InputCommunity title:string peers:Vector<InputPeer> = communities.ExportedCommunityInvite;
|
||||
//communities.exportedCommunityInvite#6b97a8ea filter:DialogFilter invite:ExportedCommunityInvite = communities.ExportedCommunityInvite;
|
||||
//exportedCommunityInvite#af7afb2f title:string url:string peers:Vector<Peer> = ExportedCommunityInvite;
|
||||
public func canShareLinkToPeer(peer: EnginePeer) -> Bool {
|
||||
var isEnabled = false
|
||||
switch peer {
|
||||
case let .channel(channel):
|
||||
if channel.adminRights != nil && channel.hasPermission(.inviteMembers) {
|
||||
isEnabled = true
|
||||
} else if channel.username != nil {
|
||||
isEnabled = true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return isEnabled
|
||||
}
|
||||
|
||||
public enum ExportChatFolderError {
|
||||
case generic
|
||||
case limitExceeded
|
||||
}
|
||||
|
||||
public struct ExportedChatFolderLink: Equatable {
|
||||
@ -47,8 +59,12 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String
|
||||
|> castError(ExportChatFolderError.self)
|
||||
|> mapToSignal { inputPeers -> Signal<ExportedChatFolderLink, ExportChatFolderError> in
|
||||
return account.network.request(Api.functions.communities.exportCommunityInvite(community: .inputCommunityDialogFilter(filterId: filterId), title: title, peers: inputPeers))
|
||||
|> mapError { _ -> ExportChatFolderError in
|
||||
return .generic
|
||||
|> mapError { error -> ExportChatFolderError in
|
||||
if error.errorDescription == "INVITES_TOO_MUCH" {
|
||||
return .limitExceeded
|
||||
} else {
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|> mapToSignal { result -> Signal<ExportedChatFolderLink, ExportChatFolderError> in
|
||||
return account.postbox.transaction { transaction -> Signal<ExportedChatFolderLink, ExportChatFolderError> in
|
||||
@ -237,6 +253,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|
||||
}
|
||||
}
|
||||
}
|
||||
alreadyMemberPeerIds.removeAll()
|
||||
|
||||
return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds)
|
||||
case let .communityInviteAlready(filterId, missingPeers, chats, users):
|
||||
@ -261,10 +278,12 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|
||||
|
||||
let currentFilters = _internal_currentChatListFilters(transaction: transaction)
|
||||
var currentFilterTitle: String?
|
||||
var currentFilterPeers: [EnginePeer.Id] = []
|
||||
if let index = currentFilters.firstIndex(where: { $0.id == filterId }) {
|
||||
switch currentFilters[index] {
|
||||
case let .filter(_, title, _, _):
|
||||
case let .filter(_, title, _, data):
|
||||
currentFilterTitle = title
|
||||
currentFilterPeers = data.includePeers.peers
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -281,6 +300,20 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|
||||
}
|
||||
}
|
||||
}
|
||||
for peerId in currentFilterPeers {
|
||||
if resultPeers.contains(where: { $0.id == peerId }) {
|
||||
continue
|
||||
}
|
||||
if let peerValue = transaction.getPeer(peerId) {
|
||||
if canShareLinkToPeer(peer: EnginePeer(peerValue)) {
|
||||
resultPeers.append(EnginePeer(peerValue))
|
||||
|
||||
if transaction.getPeerChatListIndex(peerId) != nil {
|
||||
alreadyMemberPeerIds.insert(peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds)
|
||||
}
|
||||
@ -291,6 +324,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|
||||
|
||||
public enum JoinChatFolderLinkError {
|
||||
case generic
|
||||
case limitExceeded
|
||||
}
|
||||
|
||||
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> {
|
||||
@ -300,8 +334,12 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
|
||||
|> 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
|
||||
|> mapError { error -> JoinChatFolderLinkError in
|
||||
if error.errorDescription == "DIALOG_FILTERS_TOO_MUCH" {
|
||||
return .limitExceeded
|
||||
} else {
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, JoinChatFolderLinkError> in
|
||||
account.stateManager.addUpdates(result)
|
||||
|
@ -29,6 +29,7 @@ swift_library(
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/PremiumUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -7,6 +7,81 @@ import AccountContext
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
|
||||
final class BadgeComponent: Component {
|
||||
let fillColor: UIColor
|
||||
let content: AnyComponent<Empty>
|
||||
|
||||
init(
|
||||
fillColor: UIColor,
|
||||
content: AnyComponent<Empty>
|
||||
) {
|
||||
self.fillColor = fillColor
|
||||
self.content = content
|
||||
}
|
||||
|
||||
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
|
||||
if lhs.fillColor != rhs.fillColor {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let content = ComponentView<Empty>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let height: CGFloat = 20.0
|
||||
let contentInset: CGFloat = 10.0
|
||||
|
||||
let contentSize = self.content.update(
|
||||
transition: transition,
|
||||
component: component.content,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let backgroundWidth: CGFloat = max(height, contentSize.width + contentInset)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundWidth, height: height))
|
||||
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
|
||||
transition.setBackgroundColor(layer: self.backgroundLayer, color: component.fillColor)
|
||||
transition.setCornerRadius(layer: self.backgroundLayer, cornerRadius: height / 2.0)
|
||||
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.addSubview(contentView)
|
||||
}
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
|
||||
}
|
||||
|
||||
return backgroundFrame.size
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatFolderLinkHeaderComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
@ -47,6 +122,8 @@ final class ChatFolderLinkHeaderComponent: Component {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let separatorLayer = SimpleLayer()
|
||||
|
||||
private var badge: ComponentView<Empty>?
|
||||
|
||||
private var component: ChatFolderLinkHeaderComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@ -71,6 +148,7 @@ final class ChatFolderLinkHeaderComponent: Component {
|
||||
|
||||
let height: CGFloat = 60.0
|
||||
let spacing: CGFloat = 16.0
|
||||
let badgeSpacing: CGFloat = 6.0
|
||||
|
||||
if themeUpdated {
|
||||
//TODO:localize
|
||||
@ -143,6 +221,39 @@ final class ChatFolderLinkHeaderComponent: Component {
|
||||
)
|
||||
contentWidth += titleSize.width
|
||||
|
||||
var badgeSize: CGSize?
|
||||
if let badge = component.badge {
|
||||
let badgeContainer: ComponentView<Empty>
|
||||
var badgeTransition = transition
|
||||
if let current = self.badge {
|
||||
badgeContainer = current
|
||||
} else {
|
||||
badgeTransition = .immediate
|
||||
badgeContainer = ComponentView()
|
||||
self.badge = badgeContainer
|
||||
}
|
||||
let badgeSizeValue = badgeContainer.update(
|
||||
transition: badgeTransition,
|
||||
component: AnyComponent(BadgeComponent(
|
||||
fillColor: component.theme.list.itemCheckColors.fillColor,
|
||||
content: AnyComponent(Text(text: badge, font: Font.semibold(12.0), color: component.theme.list.itemCheckColors.foregroundColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
badgeSize = badgeSizeValue
|
||||
contentWidth += badgeSpacing + badgeSizeValue.width
|
||||
} else {
|
||||
if let badge = self.badge {
|
||||
self.badge = nil
|
||||
if let view = badge.view {
|
||||
transition.setScale(view: view, scale: 0.0001, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentWidth += spacing
|
||||
if let rightImage = self.rightView.image {
|
||||
contentWidth += rightImage.size.width
|
||||
@ -163,9 +274,35 @@ final class ChatFolderLinkHeaderComponent: Component {
|
||||
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)))
|
||||
var separatorWidth = titleFrame.width
|
||||
|
||||
if let badgeSize, let badge = self.badge {
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + 1.0 + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)
|
||||
|
||||
separatorWidth += badgeSpacing + badgeSize.width
|
||||
|
||||
if let badgeView = badge.view {
|
||||
var badgeTransition = transition
|
||||
var animateIn = false
|
||||
if badgeView.superview == nil {
|
||||
badgeTransition = .immediate
|
||||
self.addSubview(badgeView)
|
||||
animateIn = true
|
||||
}
|
||||
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
|
||||
if animateIn {
|
||||
transition.animateScale(view: badgeView, from: 0.0001, to: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + 9.0), size: CGSize(width: separatorWidth, height: 3.0)))
|
||||
}
|
||||
contentOriginX += titleSize.width
|
||||
if let badgeSize {
|
||||
contentOriginX += badgeSpacing
|
||||
contentOriginX += badgeSize.width
|
||||
}
|
||||
contentOriginX += spacing
|
||||
|
||||
if let rightImage = self.rightView.image {
|
||||
|
@ -15,6 +15,7 @@ import SolidRoundedButtonComponent
|
||||
import PresentationDataUtils
|
||||
import Markdown
|
||||
import UndoUI
|
||||
import PremiumUI
|
||||
|
||||
private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -66,6 +67,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class AnimationHint {
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let dimView: UIView
|
||||
private let backgroundLayer: SimpleLayer
|
||||
@ -82,6 +88,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
private let actionButton = ComponentView<Empty>()
|
||||
|
||||
private let listHeaderText = ComponentView<Empty>()
|
||||
private let listHeaderAction = ComponentView<Empty>()
|
||||
private let itemContainerView: UIView
|
||||
private var items: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
@ -240,7 +247,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
let animateOffset: CGFloat = self.backgroundLayer.frame.minY
|
||||
if let controller = self.environment?.controller() {
|
||||
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
|
||||
let animateOffset: CGFloat = self.bounds.height - 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
|
||||
@ -254,6 +265,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
let animationHint = transition.userData(AnimationHint.self)
|
||||
|
||||
var contentTransition = transition
|
||||
if animationHint != nil {
|
||||
contentTransition = .immediate
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
|
||||
@ -282,7 +300,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
let leftButtonSize = self.leftButton.update(
|
||||
transition: transition,
|
||||
transition: contentTransition,
|
||||
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
|
||||
@ -331,19 +349,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
if titleView.superview == nil {
|
||||
self.navigationBarContainer.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
contentTransition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
|
||||
contentHeight += 44.0
|
||||
contentHeight += 14.0
|
||||
|
||||
var topBadge: String?
|
||||
if let linkContents = component.linkContents, linkContents.localFilterId != nil {
|
||||
topBadge = "+\(linkContents.peers.count)"
|
||||
}
|
||||
|
||||
let topIconSize = self.topIcon.update(
|
||||
transition: transition,
|
||||
transition: contentTransition,
|
||||
component: AnyComponent(ChatFolderLinkHeaderComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
title: component.linkContents?.title ?? "Folder",
|
||||
badge: nil
|
||||
badge: topBadge
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0)
|
||||
@ -353,7 +376,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
if topIconView.superview == nil {
|
||||
self.scrollContentView.addSubview(topIconView)
|
||||
}
|
||||
transition.setFrame(view: topIconView, frame: topIconFrame)
|
||||
contentTransition.setFrame(view: topIconView, frame: topIconFrame)
|
||||
topIconView.isHidden = component.linkContents == nil
|
||||
}
|
||||
|
||||
@ -404,7 +427,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
if descriptionTextView.superview == nil {
|
||||
self.scrollContentView.addSubview(descriptionTextView)
|
||||
}
|
||||
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
||||
contentTransition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
||||
}
|
||||
|
||||
contentHeight += descriptionTextFrame.height
|
||||
@ -446,15 +469,27 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
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 {
|
||||
guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
if self.selectedItems.contains(peer.id) {
|
||||
self.selectedItems.remove(peer.id)
|
||||
|
||||
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text: String
|
||||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
text = "You are already a member of this channel."
|
||||
} else {
|
||||
text = "You are already a member of this group."
|
||||
}
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current)
|
||||
} else {
|
||||
self.selectedItems.insert(peer.id)
|
||||
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)))
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -493,7 +528,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
listHeaderTitle = "\(self.selectedItems.count) CHATS IN FOLDER TO JOIN"
|
||||
}
|
||||
|
||||
let listHeaderActionTitle: String
|
||||
if self.selectedItems.count == self.items.count {
|
||||
listHeaderActionTitle = "DESELECT ALL"
|
||||
} else {
|
||||
listHeaderActionTitle = "SELECT ALL"
|
||||
}
|
||||
|
||||
let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor)
|
||||
let listHeaderActionBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.itemAccentColor)
|
||||
|
||||
let listHeaderTextSize = self.listHeaderText.update(
|
||||
transition: .immediate,
|
||||
@ -517,14 +560,60 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
self.scrollContentView.addSubview(listHeaderTextView)
|
||||
}
|
||||
let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize)
|
||||
transition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin)
|
||||
contentTransition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin)
|
||||
listHeaderTextView.bounds = CGRect(origin: CGPoint(), size: listHeaderTextFrame.size)
|
||||
listHeaderTextView.isHidden = component.linkContents == nil
|
||||
}
|
||||
|
||||
let listHeaderActionSize = self.listHeaderAction.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(
|
||||
text: listHeaderActionTitle,
|
||||
attributes: MarkdownAttributes(
|
||||
body: listHeaderActionBody,
|
||||
bold: listHeaderActionBody,
|
||||
link: listHeaderActionBody,
|
||||
linkAttribute: { _ in nil }
|
||||
)
|
||||
)
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let linkContents = component.linkContents else {
|
||||
return
|
||||
}
|
||||
if self.selectedItems.count != linkContents.peers.count {
|
||||
for peer in linkContents.peers {
|
||||
self.selectedItems.insert(peer.id)
|
||||
}
|
||||
} else {
|
||||
self.selectedItems.removeAll()
|
||||
for peerId in linkContents.alreadyMemberPeerIds {
|
||||
self.selectedItems.insert(peerId)
|
||||
}
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0)
|
||||
)
|
||||
if let listHeaderActionView = self.listHeaderAction.view {
|
||||
if listHeaderActionView.superview == nil {
|
||||
listHeaderActionView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.scrollContentView.addSubview(listHeaderActionView)
|
||||
}
|
||||
let listHeaderActionFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - listHeaderActionSize.width, y: contentHeight), size: listHeaderActionSize)
|
||||
contentTransition.setPosition(view: listHeaderActionView, position: CGPoint(x: listHeaderActionFrame.maxX, y: listHeaderActionFrame.minY))
|
||||
listHeaderActionView.bounds = CGRect(origin: CGPoint(), size: listHeaderActionFrame.size)
|
||||
listHeaderActionView.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)))
|
||||
contentTransition.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))
|
||||
@ -536,7 +625,11 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
let actionButtonTitle: String
|
||||
if let linkContents = component.linkContents {
|
||||
if linkContents.localFilterId != nil {
|
||||
actionButtonTitle = "Join Chats"
|
||||
if self.selectedItems.isEmpty {
|
||||
actionButtonTitle = "Do Not Join Any Chats"
|
||||
} else {
|
||||
actionButtonTitle = "Join Chats"
|
||||
}
|
||||
} else {
|
||||
actionButtonTitle = "Add Folder"
|
||||
}
|
||||
@ -555,25 +648,41 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
height: 50.0,
|
||||
cornerRadius: 11.0,
|
||||
gloss: false,
|
||||
isEnabled: !self.selectedItems.isEmpty,
|
||||
isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil,
|
||||
animationName: nil,
|
||||
iconPosition: .right,
|
||||
iconSpacing: 4.0,
|
||||
isLoading: component.linkContents == nil,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() 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
|
||||
|> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
switch error {
|
||||
case .generic:
|
||||
controller.dismiss()
|
||||
case .limitExceeded:
|
||||
//TODO:localize
|
||||
let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: 5, action: {})
|
||||
controller.push(limitController)
|
||||
controller.dismiss()
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
guard let self, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
})
|
||||
} else {
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,6 +722,19 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||
}
|
||||
|
||||
if let controller = environment.controller() {
|
||||
let subLayout = ContainerViewLayout(
|
||||
size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset),
|
||||
safeInsets: UIEdgeInsets(),
|
||||
additionalInsets: UIEdgeInsets(),
|
||||
statusBarHeight: nil,
|
||||
inputHeight: nil,
|
||||
inputHeightIsInteractivellyChanging: false,
|
||||
inVoiceOver: false
|
||||
)
|
||||
controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition)
|
||||
}
|
||||
|
||||
contentHeight += bottomPanelHeight
|
||||
initialContentHeight += bottomPanelHeight
|
||||
|
||||
@ -676,14 +798,23 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.linkContentsDisposable = (context.engine.peers.checkChatFolderLink(slug: slug)
|
||||
//|> delay(0.2, queue: .mainQueue())
|
||||
|> 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)
|
||||
self.updateComponent(component: AnyComponent(ChatFolderLinkPreviewScreenComponent(context: context, slug: slug, linkContents: result)), transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)).withUserData(ChatFolderLinkPreviewScreenComponent.AnimationHint()))
|
||||
}, error: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "The folder link has expired."), elevatedLayout: false, action: { _ in true }), in: .window(.root))
|
||||
self.dismiss()
|
||||
})
|
||||
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
@ -694,6 +825,10 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
|
||||
self.linkContentsDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user