Folder updates

This commit is contained in:
Ali 2023-03-28 23:41:04 +04:00
parent 8ca477e686
commit 4ccd0bd804
23 changed files with 606 additions and 237 deletions

View File

@ -90,6 +90,7 @@ swift_library(
"//submodules/AvatarVideoNode:AvatarVideoNode",
"//submodules/InviteLinksUI",
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
"//submodules/ItemListUI",
],
visibility = [
"//visibility:public",

View File

@ -1547,8 +1547,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
for filter in filters {
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)
items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
c.dismiss(completion: {
guard let strongSelf = self else {
@ -2701,6 +2701,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) {
openCreateChatListFolderLink(context: self.context, folderId: filterId, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in
self?.push(c)
}, presentController: { [weak self] c in
self?.present(c, in: .window(.root))
}, linkUpdated: { _ in
})
/*self.push(folderInviteLinkListController(context: self.context, filterId: filterId, title: title, allPeerIds: data.includePeers.peers, currentInvitation: nil, linkUpdated: { _ in
}))*/
}

View File

@ -186,7 +186,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {})
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {})
interaction.isInlineMode = isInlineMode
let items = (0 ..< 2).map { _ -> ChatListItem in

View File

@ -519,7 +519,7 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case .inviteLinkHeader:
//TODO:localize
return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section)
case let.inviteLinkCreate(hasLinks):
case let .inviteLinkCreate(hasLinks):
//TODO:localize
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()
@ -654,14 +654,21 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
}
if !isNewFilter, let inviteLinks {
if !isNewFilter {
entries.append(.inviteLinkHeader)
entries.append(.inviteLinkCreate(hasLinks: !inviteLinks.isEmpty))
var index = 0
for link in inviteLinks {
entries.append(.inviteLink(index, link))
index += 1
var hasLinks = false
if let inviteLinks, !inviteLinks.isEmpty {
hasLinks = true
}
entries.append(.inviteLinkCreate(hasLinks: hasLinks))
if let inviteLinks {
var index = 0
for link in inviteLinks {
entries.append(.inviteLink(index, link))
index += 1
}
}
entries.append(.inviteLinkInfo)
@ -1065,6 +1072,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var focusOnNameImpl: (() -> Void)?
var applyImpl: ((@escaping () -> Void) -> Void)?
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
if let currentPreset {
@ -1273,114 +1281,77 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
},
createLink: {
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 let .limitExceeded(limit, premiumLimit):
if limit < premiumLimit {
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: {
})
pushControllerImpl?(limitController)
return
}
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 _ = currentPreset.data {
applyImpl?({
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 {
if let currentPreset, let data = currentPreset.data {
//TODO:localize
var unavailableText: String?
if !data.categories.isEmpty || data.excludeArchived || data.excludeRead || data.excludeMuted || !data.excludePeers.isEmpty {
unavailableText = "You can't share a link to this folder."
}
if let unavailableText {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
return
}
openCreateChatListFolderLink(context: context, folderId: currentPreset.id, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in
pushControllerImpl?(c)
}, presentController: { c in
presentControllerImpl?(c, nil)
}, linkUpdated: { updatedLink in
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)
if let index = links.firstIndex(where: { $0.link == updatedLink.link }) {
links[index] = updatedLink
} else {
links.insert(updatedLink, at: 0)
}
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))
}
}
})
}
}))
})
}
})
}, openLink: { link in
if let currentPreset, let _ = currentPreset.data {
applyImpl?({
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 {
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))
}
}
})
}
}))
})
}
}
)
var attemptNavigationImpl: (() -> Bool)?
let applyImpl: (() -> Void)? = {
applyImpl = { completed in
let state = stateValue.with { $0 }
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var includePeers = ChatListFilterIncludePeers()
@ -1422,7 +1393,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
|> deliverOnMainQueue).start(next: { filters in
updated(filters)
dismissImpl?()
completed()
})
}
@ -1449,7 +1420,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
})
let rightNavigationButton = ItemListNavigationButton(content: .text(currentPreset == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: {
applyImpl?()
applyImpl?({
dismissImpl?()
})
})
let previousStateValue = previousState
@ -1531,3 +1504,42 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
return controller
}
func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, title: String, peerIds: [EnginePeer.Id], pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, linkUpdated: @escaping (ExportedChatFolderLink?) -> Void) {
if peerIds.isEmpty {
return
}
let _ = (context.engine.data.get(
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
)
|> deliverOnMainQueue).start(next: { peers in
let peers = peers.compactMap({ $0 })
if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) {
pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: peerIds, currentInvitation: nil, linkUpdated: linkUpdated))
} else {
let _ = (context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: peerIds)
|> deliverOnMainQueue).start(next: { link in
linkUpdated(link)
pushController(folderInviteLinkListController(context: context, filterId: folderId, title: title, allPeerIds: link.peerIds, currentInvitation: link, linkUpdated: linkUpdated))
}, error: { error in
//TODO:localize
let text: String
switch error {
case .generic:
text = "An error occurred"
case let .limitExceeded(limit, premiumLimit):
if limit < premiumLimit {
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: {
})
pushController(limitController)
return
}
text = "You can't create more links."
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))
})
}
})
}

View File

@ -2165,6 +2165,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openPasswordSetup: {
}, openPremiumIntro: {
}, openChatFolderUpdates: {
}, hideChatFolderUpdates: {
})
chatListInteraction.isSearchMode = true
@ -3398,7 +3399,8 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {})
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
})
var isInlineMode = false
if case .topics = key {
isInlineMode = false

View File

@ -97,6 +97,7 @@ public final class ChatListNodeInteraction {
let openPasswordSetup: () -> Void
let openPremiumIntro: () -> Void
let openChatFolderUpdates: () -> Void
let hideChatFolderUpdates: () -> Void
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
@ -142,7 +143,8 @@ public final class ChatListNodeInteraction {
openStorageManagement: @escaping () -> Void,
openPasswordSetup: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void,
openChatFolderUpdates: @escaping () -> Void
openChatFolderUpdates: @escaping () -> Void,
hideChatFolderUpdates: @escaping () -> Void
) {
self.activateSearch = activateSearch
self.peerSelected = peerSelected
@ -176,6 +178,7 @@ public final class ChatListNodeInteraction {
self.openPasswordSetup = openPasswordSetup
self.openPremiumIntro = openPremiumIntro
self.openChatFolderUpdates = openChatFolderUpdates
self.hideChatFolderUpdates = hideChatFolderUpdates
}
}
@ -616,16 +619,26 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
case let .ArchiveIntro(presentationData):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
case let .Notice(presentationData, notice):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] in
switch notice {
case .clearStorage:
nodeInteraction?.openStorageManagement()
case .setupPassword:
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
switch action {
case .activate:
switch notice {
case .clearStorage:
nodeInteraction?.openStorageManagement()
case .setupPassword:
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
}
case .hide:
switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default:
break
}
}
}), directionHint: entry.directionHint)
}
@ -871,16 +884,26 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
case let .ArchiveIntro(presentationData):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
case let .Notice(presentationData, notice):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] in
switch notice {
case .clearStorage:
nodeInteraction?.openStorageManagement()
case .setupPassword:
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
switch action {
case .activate:
switch notice {
case .clearStorage:
nodeInteraction?.openStorageManagement()
case .setupPassword:
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
}
case .hide:
switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default:
break
}
}
}), directionHint: entry.directionHint)
case .HeaderEntry:
@ -1076,8 +1099,9 @@ public final class ChatListNode: ListView {
let hideArhiveIntro = ValuePromise<Bool>(false, ignoreRepeated: true)
private let chatFolderUpdates = Promise<ChatFolderUpdates?>(nil)
private let chatFolderUpdates = Promise<ChatFolderUpdates?>()
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
self.context = context
@ -1413,6 +1437,21 @@ public final class ChatListNode: ListView {
self.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(result), contents: result.chatFolderLinkContents))
})
}, hideChatFolderUpdates: { [weak self] in
guard let self else {
return
}
let _ = (self.chatFolderUpdates.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, let result else {
return
}
if let localFilterId = result.chatFolderLinkContents.localFilterId {
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: localFilterId).start()
}
})
})
nodeInteraction.isInlineMode = isInlineMode
@ -1973,6 +2012,7 @@ public final class ChatListNode: ListView {
var didIncludeRemovingPeerId = false
var didIncludeHiddenByDefaultArchive = false
var didIncludeHiddenThread = false
var didIncludeNotice = false
if let previous = previousView {
for entry in previous.filteredEntries {
if case let .PeerEntry(peerEntry) = entry {
@ -1999,12 +2039,15 @@ public final class ChatListNode: ListView {
}
} else if case let .GroupReferenceEntry(_, _, _, _, _, _, _, _, hiddenByDefault) = entry {
didIncludeHiddenByDefaultArchive = hiddenByDefault
} else if case .Notice = entry {
didIncludeNotice = true
}
}
}
var doesIncludeRemovingPeerId = false
var doesIncludeArchive = false
var doesIncludeHiddenByDefaultArchive = false
var doesIncludeNotice = false
var doesIncludeHiddenThread = false
for entry in processedView.filteredEntries {
@ -2033,6 +2076,8 @@ public final class ChatListNode: ListView {
} else if case let .GroupReferenceEntry(_, _, _, _, _, _, _, _, hiddenByDefault) = entry {
doesIncludeArchive = true
doesIncludeHiddenByDefaultArchive = hiddenByDefault
} else if case .Notice = entry {
doesIncludeNotice = true
}
}
if previousPinnedChats != updatedPinnedChats || previousPinnedThreads != updatedPinnedThreads {
@ -2059,6 +2104,9 @@ public final class ChatListNode: ListView {
if didIncludeHiddenThread != doesIncludeHiddenThread {
disableAnimations = false
}
if didIncludeNotice != doesIncludeNotice {
disableAnimations = false
}
}
if let _ = previousHideArchivedFolderByDefaultValue, previousHideArchivedFolderByDefaultValue != hideArchivedFolderByDefault {
@ -2578,7 +2626,7 @@ public final class ChatListNode: ListView {
}
}
self.pollFilterUpdates(shouldDelay: false)
self.pollFilterUpdates()
self.resetFilter()
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
@ -2596,6 +2644,7 @@ public final class ChatListNode: ListView {
self.activityStatusesDisposable?.dispose()
self.updatedFilterDisposable.dispose()
self.pollFilterUpdatesDisposable?.dispose()
self.chatFilterUpdatesDisposable?.dispose()
}
func updateFilter(_ filter: ChatListFilter?) {
@ -2605,17 +2654,18 @@ public final class ChatListNode: ListView {
}
}
private func pollFilterUpdates(shouldDelay: Bool) {
private func pollFilterUpdates() {
guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else {
self.chatFolderUpdates.set(.single(nil))
return
}
self.pollFilterUpdatesDisposable = (context.engine.peers.getChatFolderUpdates(folderId: id)
|> delay(shouldDelay ? 5.0 : 0.0, queue: .mainQueue())).start(next: { [weak self] result in
self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start()
self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.chatFolderUpdates.set(.single(result))
self.pollFilterUpdates(shouldDelay: true)
})
}

View File

@ -7,16 +7,22 @@ import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import AppBundle
import ItemListUI
class ChatListStorageInfoItem: ListViewItem {
enum Action {
case activate
case hide
}
let theme: PresentationTheme
let strings: PresentationStrings
let notice: ChatListNotice
let action: () -> Void
let action: (Action) -> Void
let selectable: Bool = true
init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping () -> Void) {
init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) {
self.theme = theme
self.strings = strings
self.notice = notice
@ -26,7 +32,7 @@ class ChatListStorageInfoItem: ListViewItem {
func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.action()
self.action(.activate)
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -72,7 +78,8 @@ private let separatorHeight = 1.0 / UIScreen.main.scale
private let titleFont = Font.semibold(15.0)
private let textFont = Font.regular(15.0)
class ChatListStorageInfoItemNode: ListViewItemNode {
class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
private let contentContainer: ASDisplayNode
private let titleNode: TextNode
private let textNode: TextNode
private let arrowNode: ASImageNode
@ -81,17 +88,23 @@ class ChatListStorageInfoItemNode: ListViewItemNode {
private var item: ChatListStorageInfoItem?
required init() {
self.contentContainer = ASDisplayNode()
self.titleNode = TextNode()
self.textNode = TextNode()
self.arrowNode = ASImageNode()
self.separatorNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.clipsToBounds = true
self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
self.contentContainer.addSubnode(self.titleNode)
self.contentContainer.addSubnode(self.textNode)
self.contentContainer.addSubnode(self.arrowNode)
self.addSubnode(self.contentContainer)
self.zPosition = 1.0
}
@ -201,8 +214,35 @@ class ChatListStorageInfoItemNode: ListViewItemNode {
strongSelf.contentSize = layout.contentSize
strongSelf.insets = layout.insets
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
switch item.notice {
case .chatFolderUpdates:
//TODO:locallize
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: "Hide", icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
default:
strongSelf.setRevealOptions((left: [], right: []))
}
}
})
}
}
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
transition.updateSublayerTransformOffset(layer: self.contentContainer.layer, offset: CGPoint(x: offset, y: 0.0))
}
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.item {
item.action(.hide)
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}

View File

@ -75,13 +75,20 @@ public enum ContextMenuActionBadgeColor {
case inactive
}
public struct ContextMenuActionBadge {
public struct ContextMenuActionBadge: Equatable {
public enum Style {
case badge
case label
}
public var value: String
public var color: ContextMenuActionBadgeColor
public var style: Style
public init(value: String, color: ContextMenuActionBadgeColor) {
public init(value: String, color: ContextMenuActionBadgeColor, style: Style = .badge) {
self.value = value
self.color = color
self.style = style
}
}

View File

@ -64,8 +64,11 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
private let titleLabelNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let iconNode: ASImageNode
private var badgeIconNode: ASImageNode?
private var animationNode: AnimationNode?
private var currentBadge: (badge: ContextMenuActionBadge, image: UIImage)?
private var iconDisposable: Disposable?
init(
@ -302,14 +305,66 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
iconSize = iconImage?.size
}
let badgeSize: CGSize?
if let badge = self.item.badge {
var badgeImage: UIImage?
if let currentBadge = self.currentBadge, currentBadge.badge == badge {
badgeImage = currentBadge.image
} else {
let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor
let badgeString = NSAttributedString(string: badge.value, font: Font.semibold(11.0), textColor: badgeTextColor)
let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
let badgeSideInset: CGFloat = 3.0
let badgeVerticalInset: CGFloat = 1.0
let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height))
badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath)
context.fillPath()
UIGraphicsPushContext(context)
badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + badgeSideInset + UIScreenPixel, y: badgeTextBounds.minY + badgeVerticalInset + UIScreenPixel))
UIGraphicsPopContext()
})
}
let badgeIconNode: ASImageNode
if let current = self.badgeIconNode {
badgeIconNode = current
} else {
badgeIconNode = ASImageNode()
self.badgeIconNode = badgeIconNode
self.addSubnode(badgeIconNode)
}
badgeIconNode.image = badgeImage
badgeSize = badgeImage?.size
} else {
if let badgeIconNode = self.badgeIconNode {
self.badgeIconNode = nil
badgeIconNode.removeFromSupernode()
}
badgeSize = nil
}
var maxTextWidth: CGFloat = constrainedSize.width
maxTextWidth -= sideInset
if let iconSize = iconSize {
maxTextWidth -= max(standardIconWidth, iconSize.width)
maxTextWidth -= iconSpacing
} else {
maxTextWidth -= sideInset
}
if let badgeSize = badgeSize {
maxTextWidth -= badgeSize.width
maxTextWidth -= 8.0
}
maxTextWidth = max(1.0, maxTextWidth)
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 1000.0))
@ -351,6 +406,12 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame)
transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame)
if let badgeIconNode = self.badgeIconNode {
if let iconSize = badgeIconNode.image?.size {
transition.updateFrame(node: badgeIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 8.0, y: titleFrame.minY + floor((titleFrame.height - iconSize.height) * 0.5)), size: iconSize))
}
}
if let iconSize = iconSize {
let iconWidth = max(standardIconWidth, iconSize.width)
let iconFrame = CGRect(

View File

@ -95,6 +95,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, openPasswordSetup: {
}, openPremiumIntro: {
}, openChatFolderUpdates: {
}, hideChatFolderUpdates: {
})
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)

View File

@ -118,6 +118,7 @@ private class PremiumLimitAnimationComponent: Component {
self.activeContainer.masksToBounds = true
self.activeBackground = SimpleLayer()
self.activeBackground.anchorPoint = CGPoint()
self.badgeView = UIView()
self.badgeView.alpha = 0.0
@ -244,13 +245,16 @@ private class PremiumLimitAnimationComponent: Component {
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight))
self.container.frame = containerFrame
let activityPosition: CGFloat = floor(containerFrame.width * component.badgePosition)
let activeWidth: CGFloat = containerFrame.width - activityPosition
if !component.isPremiumDisabled {
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: containerFrame.width / 2.0, height: lineHeight))
self.activeContainer.frame = CGRect(origin: CGPoint(x: containerFrame.width / 2.0, y: 0.0), size: CGSize(width: containerFrame.width / 2.0, height: lineHeight))
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))
self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))
self.activeBackground.bounds = CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 3.0 / 2.0, height: lineHeight))
self.activeBackground.frame = CGRect(origin: .zero, size: CGSize(width: activeWidth * (1.0 + 0.35), height: lineHeight))
if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: containerFrame.width * 3.0 / 4.0 - self.activeBackground.frame.width * 0.35, y: lineHeight / 2.0)
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
}
}
@ -306,7 +310,7 @@ private class PremiumLimitAnimationComponent: Component {
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * badgePosition, y: 82.0)
self.badgeView.center = CGPoint(x: availableSize.width * badgePosition, y: 82.0)
}
if self.badgeView.frame.maxX > availableSize.width {
@ -375,14 +379,16 @@ private class PremiumLimitAnimationComponent: Component {
}
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
let lineOffset = (self.activeBackground.frame.width - self.activeContainer.bounds.width) / 2.0
let lineOffset = 0.0
let linePreviousValue = self.activeBackground.position.x
var lineNewValue: CGFloat = lineOffset
if lineOffset - linePreviousValue < self.activeBackground.frame.width * 0.25 {
lineNewValue -= self.activeBackground.frame.width * 0.35
if linePreviousValue < 0.0 {
lineNewValue = 0.0
} else {
lineNewValue = -self.activeContainer.bounds.width * 0.35
}
self.activeBackground.position = CGPoint(x: lineNewValue, y: self.activeBackground.bounds.size.height / 2.0)
self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0)
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
badgeAnimation.duration = 4.5
badgeAnimation.fromValue = badgePreviousValue
@ -585,16 +591,25 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
transition: context.transition
)
let activityPosition = floor(context.availableSize.width * component.badgePosition)
var inactiveValueOpacity: CGFloat = 1.0
if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 {
inactiveValueOpacity = 0.0
}
context.add(inactiveTitle
.position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
.opacity(inactiveValueOpacity)
)
context.add(inactiveValue
.position(CGPoint(x: context.availableSize.width / 2.0 - inactiveValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0))
.position(CGPoint(x: activityPosition - inactiveValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0))
.opacity(inactiveValueOpacity)
)
context.add(activeTitle
.position(CGPoint(x: context.availableSize.width / 2.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
.position(CGPoint(x: activityPosition + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
)
context.add(activeValue
@ -766,22 +781,26 @@ private final class LimitSheetContent: CombinedComponent {
string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string
}
case .linksPerSharedFolder:
//TODO:localize
let limit = state.limits.maxSharedFolderInviteLinks
let premiumLimit = state.premiumLimits.maxSharedFolderInviteLinks
/*let count: Int32 = 5 + Int32("".count)// component.count
let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/
let count: Int32 = component.count
let limit: Int32 = state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks
iconName = "Premium/Link"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeText = "\(count)"
string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
defaultValue = count > limit ? "\(limit)" : ""
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string
}
case .membershipInSharedFolders:
//TODO:localize
let limit = state.limits.maxSharedFolderJoin
let premiumLimit = state.premiumLimits.maxSharedFolderJoin
iconName = "Premium/Folder"

View File

@ -47,7 +47,7 @@ public final class QrCodeScreen: ViewController {
case let .invite(invite, _):
return invite.link ?? ""
case let .chatFolder(slug):
return "https://t.me/folder/\(slug)"
return "https://t.me/list/\(slug)"
}
}

View File

@ -222,7 +222,8 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {})
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View File

@ -843,7 +843,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {})
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
func makeChatListItem(

View File

@ -367,7 +367,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {})
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
})
func makeChatListItem(
peer: EnginePeer,

View File

@ -997,7 +997,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) }
dict[-266911767] = { return Api.channels.ChannelParticipants.parse_channelParticipantsNotModified($0) }
dict[-191450938] = { return Api.channels.SendAsPeers.parse_sendAsPeers($0) }
dict[-557919187] = { return Api.communities.CommunityInvite.parse_communityInvite($0) }
dict[59080097] = { return Api.communities.CommunityInvite.parse_communityInvite($0) }
dict[-951718393] = { return Api.communities.CommunityInvite.parse_communityInviteAlready($0) }
dict[-414818125] = { return Api.communities.CommunityUpdates.parse_communityUpdates($0) }
dict[1805101290] = { return Api.communities.ExportedCommunityInvite.parse_exportedCommunityInvite($0) }

View File

@ -896,16 +896,18 @@ public extension Api.channels {
}
public extension Api.communities {
enum CommunityInvite: TypeConstructorDescription {
case communityInvite(title: String, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
case communityInvite(flags: Int32, title: String, emoticon: String?, peers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
case communityInviteAlready(filterId: Int32, missingPeers: [Api.Peer], alreadyPeers: [Api.Peer], chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .communityInvite(let title, let peers, let chats, let users):
case .communityInvite(let flags, let title, let emoticon, let peers, let chats, let users):
if boxed {
buffer.appendInt32(-557919187)
buffer.appendInt32(59080097)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(peers.count))
for item in peers {
@ -953,34 +955,40 @@ public extension Api.communities {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
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 .communityInvite(let flags, let title, let emoticon, let peers, let chats, let users):
return ("communityInvite", [("flags", flags as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)])
case .communityInviteAlready(let filterId, let missingPeers, let alreadyPeers, let chats, let users):
return ("communityInviteAlready", [("filterId", filterId as Any), ("missingPeers", missingPeers as Any), ("alreadyPeers", alreadyPeers as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
public static func parse_communityInvite(_ reader: BufferReader) -> CommunityInvite? {
var _1: String?
_1 = parseString(reader)
var _2: [Api.Peer]?
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
_2 = parseString(reader)
var _3: String?
if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
var _4: [Api.Peer]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self)
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self)
}
var _3: [Api.Chat]?
var _5: [Api.Chat]?
if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _4: [Api.User]?
var _6: [Api.User]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.communities.CommunityInvite.communityInvite(title: _1!, peers: _2!, chats: _3!, users: _4!)
let _c5 = _5 != nil
let _c6 = _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.communities.CommunityInvite.communityInvite(flags: _1!, title: _2!, emoticon: _3, peers: _4!, chats: _5!, users: _6!)
}
else {
return nil

View File

@ -256,6 +256,7 @@ private enum PreferencesKeyValues: Int32 {
case globalMessageAutoremoveTimeoutSettings = 27
case accountSpecificCacheStorageSettings = 28
case linksConfiguration = 29
case chatListFilterUpdates = 30
}
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
@ -402,6 +403,12 @@ public struct PreferencesKeys {
key.setInt32(0, value: PreferencesKeyValues.linksConfiguration.rawValue)
return key
}()
public static let chatListFilterUpdates: ValueBoxKey = {
let key = ValueBoxKey(length: 4)
key.setInt32(0, value: PreferencesKeyValues.chatListFilterUpdates.rawValue)
return key
}()
}
private enum SharedDataKeyValues: Int32 {

View File

@ -872,14 +872,29 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
}
struct ChatListFiltersState: Codable, Equatable {
struct ChatListFilterUpdates: Codable, Equatable {
var folderId: Int32
var timestamp: Int32
var peerIds: [PeerId]
init(folderId: Int32, timestamp: Int32, peerIds: [PeerId]) {
self.folderId = folderId
self.timestamp = timestamp
self.peerIds = peerIds
}
}
var filters: [ChatListFilter]
var remoteFilters: [ChatListFilter]?
static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil)
var updates: [ChatListFilterUpdates]
fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?) {
static var `default` = ChatListFiltersState(filters: [], remoteFilters: nil, updates: [])
fileprivate init(filters: [ChatListFilter], remoteFilters: [ChatListFilter]?, updates: [ChatListFilterUpdates]) {
self.filters = filters
self.remoteFilters = remoteFilters
self.updates = updates
}
public init(from decoder: Decoder) throws {
@ -887,6 +902,7 @@ struct ChatListFiltersState: Codable, Equatable {
self.filters = try container.decode([ChatListFilter].self, forKey: "filters")
self.remoteFilters = try container.decodeIfPresent([ChatListFilter].self, forKey: "remoteFilters")
self.updates = try container.decodeIfPresent([ChatListFilterUpdates].self, forKey: "updates") ?? []
}
func encode(to encoder: Encoder) throws {
@ -894,6 +910,14 @@ struct ChatListFiltersState: Codable, Equatable {
try container.encode(self.filters, forKey: "filters")
try container.encodeIfPresent(self.remoteFilters, forKey: "remoteFilters")
try container.encode(self.updates, forKey: "updates")
}
mutating func normalize() {
if self.updates.isEmpty {
return
}
self.updates.removeAll(where: { update in !self.filters.contains(where: { $0.id == update.folderId }) })
}
}
@ -918,6 +942,9 @@ func _internal_updateChatListFiltersInteractively(postbox: Postbox, _ f: @escapi
hasUpdates = true
}
updated = updatedFilters
state.normalize()
return PreferencesEntry(state)
})
if hasUpdates {
@ -936,6 +963,7 @@ func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f:
state.filters = updatedFilters
hasUpdates = true
}
state.normalize()
return PreferencesEntry(state)
})
if hasUpdates {
@ -943,7 +971,6 @@ func _internal_updateChatListFiltersInteractively(transaction: Transaction, _ f:
}
}
func _internal_updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilter], NoError> {
return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters])
|> map { preferences -> [ChatListFilter] in
@ -953,6 +980,15 @@ func _internal_updatedChatListFilters(postbox: Postbox) -> Signal<[ChatListFilte
|> distinctUntilChanged
}
func _internal_updatedChatListFiltersState(postbox: Postbox) -> Signal<ChatListFiltersState, NoError> {
return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters])
|> map { preferences -> ChatListFiltersState in
let filtersState = preferences.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
return filtersState
}
|> distinctUntilChanged
}
func _internal_updatedChatListFiltersInfo(postbox: Postbox) -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> {
return postbox.preferencesView(keys: [PreferencesKeys.chatListFilters])
|> map { preferences -> (filters: [ChatListFilter], synchronized: Bool) in
@ -982,11 +1018,17 @@ func _internal_currentChatListFilters(transaction: Transaction) -> [ChatListFilt
return settings.filters
}
func _internal_currentChatListFiltersState(transaction: Transaction) -> ChatListFiltersState {
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.chatListFilters)?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
return settings
}
func updateChatListFiltersState(transaction: Transaction, _ f: (ChatListFiltersState) -> ChatListFiltersState) -> ChatListFiltersState {
var result: ChatListFiltersState?
transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFilters, { entry in
let settings = entry?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default
let updated = f(settings)
var updated = f(settings)
updated.normalize()
result = updated
return PreferencesEntry(updated)
})

View File

@ -60,7 +60,7 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String
|> mapToSignal { inputPeers -> Signal<ExportedChatFolderLink, ExportChatFolderError> in
return account.network.request(Api.functions.communities.exportCommunityInvite(community: .inputCommunityDialogFilter(filterId: filterId), title: title, peers: inputPeers))
|> `catch` { error -> Signal<Api.communities.ExportedCommunityInvite, ExportChatFolderError> in
if error.errorDescription == "INVITES_TOO_MUCH" {
if error.errorDescription == "INVITES_TOO_MUCH" || error.errorDescription == "FILTERS_TOO_MUCH" {
return account.postbox.transaction { transaction -> (AppConfiguration, Bool) in
return (currentAppConfiguration(transaction: transaction), transaction.getPeer(account.peerId)?.isPremium ?? false)
}
@ -69,10 +69,18 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String
let userDefaultLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false)
let userPremiumLimits = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true)
if isPremium {
return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks))
if error.errorDescription == "FILTERS_TOO_MUCH" {
if isPremium {
return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin))
} else {
return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin))
}
} else {
return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks))
if isPremium {
return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks))
} else {
return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderInviteLinks, premiumLimit: userPremiumLimits.maxSharedFolderInviteLinks))
}
}
}
} else {
@ -246,7 +254,9 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
|> mapToSignal { result -> Signal<ChatFolderLinkContents, CheckChatFolderLinkError> in
return account.postbox.transaction { transaction -> ChatFolderLinkContents in
switch result {
case let .communityInvite(title, peers, chats, users):
case let .communityInvite(_, title, emoticon, peers, chats, users):
let _ = emoticon
var allPeers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
@ -321,7 +331,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
if let peerValue = transaction.getPeer(peer.peerId) {
resultPeers.append(EnginePeer(peerValue))
if transaction.getPeerChatListIndex(peer.peerId) != nil {
if currentFilterPeers.contains(where: { $0 == peer.peerId }) && transaction.getPeerChatListIndex(peer.peerId) != nil {
alreadyMemberPeerIds.insert(peer.peerId)
}
}
@ -407,74 +417,148 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
public final class ChatFolderUpdates: Equatable {
fileprivate let folderId: Int32
fileprivate let title: String
fileprivate let missingPeers: [Api.Peer]
fileprivate let chats: [Api.Chat]
fileprivate let users: [Api.User]
fileprivate let missingPeers: [EnginePeer]
public var availableChatsToJoin: Int {
return self.missingPeers.count
}
public var chatFolderLinkContents: ChatFolderLinkContents {
var peers: [EnginePeer] = []
for missingPeer in self.missingPeers {
for chat in chats {
if chat.peerId == missingPeer.peerId {
if let peer = parseTelegramGroupOrChannel(chat: chat) {
peers.append(EnginePeer(peer))
}
}
}
}
return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: peers, alreadyMemberPeerIds: Set())
return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set())
}
fileprivate init(
folderId: Int32,
title: String,
missingPeers: [Api.Peer],
chats: [Api.Chat],
users: [Api.User]
missingPeers: [EnginePeer]
) {
self.folderId = folderId
self.title = title
self.missingPeers = missingPeers
self.chats = chats
self.users = users
}
public static func ==(lhs: ChatFolderUpdates, rhs: ChatFolderUpdates) -> Bool {
if lhs.folderId != rhs.folderId {
return false
}
if lhs.missingPeers.map(\.peerId) != rhs.missingPeers.map(\.peerId) {
if lhs.missingPeers.map(\.id) != rhs.missingPeers.map(\.id) {
return false
}
return true
}
}
func _internal_getChatFolderUpdates(account: Account, folderId: Int32) -> Signal<ChatFolderUpdates?, NoError> {
return account.network.request(Api.functions.communities.getCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.communities.CommunityUpdates?, NoError> in
return .single(nil)
func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> ChatListFiltersState in
return _internal_currentChatListFiltersState(transaction: transaction)
}
|> mapToSignal { result -> Signal<ChatFolderUpdates?, NoError> in
guard let result = result else {
|> mapToSignal { state -> Signal<Never, NoError> in
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let current = state.updates.first(where: { $0.folderId == folderId }) {
let updateInterval: Int32
#if DEBUG
updateInterval = 5
#else
updateInterval = 60 * 60
#endif
if current.timestamp + updateInterval >= timestamp {
return .complete()
}
}
return account.network.request(Api.functions.communities.getCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.communities.CommunityUpdates?, NoError> in
return .single(nil)
}
switch result {
case let .communityUpdates(missingPeers, chats, users):
return account.postbox.transaction { transaction -> ChatFolderUpdates? in
for filter in _internal_currentChatListFilters(transaction: transaction) {
if case let .filter(id, title, _, _) = filter, id == folderId {
return ChatFolderUpdates(folderId: folderId, title: title, missingPeers: missingPeers, chats: chats, users: users)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result = result else {
return account.postbox.transaction { transaction -> Void in
let _ = updateChatListFiltersState(transaction: transaction, { state in
var state = state
state.updates.removeAll(where: { $0.folderId == folderId })
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: []))
return state
})
}
return nil
|> ignoreValues
}
switch result {
case let .communityUpdates(missingPeers, chats, users):
return account.postbox.transaction { transaction -> Void in
var peers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
peerPresences[telegramUser.id] = user
}
for chat in chats {
if let peer = parseTelegramGroupOrChannel(chat: chat) {
peers.append(peer)
}
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: peerPresences)
let _ = updateChatListFiltersState(transaction: transaction, { state in
var state = state
state.updates.removeAll(where: { $0.folderId == folderId })
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId)))
return state
})
}
|> ignoreValues
}
}
}
}
func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> Signal<ChatFolderUpdates?, NoError> {
struct InternalData: Equatable {
var title: String
var peerIds: [EnginePeer.Id]
}
return _internal_updatedChatListFiltersState(postbox: account.postbox)
|> map { state -> InternalData? in
guard let update = state.updates.first(where: { $0.folderId == folderId }) else {
return nil
}
guard let folder = state.filters.first(where: { $0.id == folderId }) else {
return nil
}
guard case let .filter(_, title, _, data) = folder, data.isShared else {
return nil
}
let filteredPeerIds: [PeerId] = update.peerIds.filter { !data.includePeers.peers.contains($0) }
return InternalData(title: title, peerIds: filteredPeerIds)
}
|> distinctUntilChanged
|> mapToSignal { internalData -> Signal<ChatFolderUpdates?, NoError> in
guard let internalData = internalData else {
return .single(nil)
}
if internalData.peerIds.isEmpty {
return .single(nil)
}
return account.postbox.transaction { transaction -> ChatFolderUpdates? in
var peers: [EnginePeer] = []
for peerId in internalData.peerIds {
if let peer = transaction.getPeer(peerId) {
peers.append(EnginePeer(peer))
}
}
return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers)
}
}
}
@ -530,11 +614,22 @@ func _internal_joinAvailableChatsInFolder(account: Account, updates: ChatFolderU
}
func _internal_hideChatFolderUpdates(account: Account, folderId: Int32) -> Signal<Never, NoError> {
return account.network.request(Api.functions.communities.hideCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
return account.postbox.transaction { transaction -> Void in
let _ = updateChatListFiltersState(transaction: transaction, { state in
var state = state
state.updates.removeAll(where: { $0.folderId == folderId })
return state
})
}
|> mapToSignal { _ -> Signal<Never, NoError> in
return account.network.request(Api.functions.communities.hideCommunityUpdates(community: .inputCommunityDialogFilter(filterId: folderId)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
|> ignoreValues
}
func _internal_leaveChatFolder(account: Account, folderId: Int32, removePeerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {

View File

@ -1050,8 +1050,20 @@ public extension TelegramEngine {
return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds)
}
public func getChatFolderUpdates(folderId: Int32) -> Signal<ChatFolderUpdates?, NoError> {
return _internal_getChatFolderUpdates(account: self.account, folderId: folderId)
public func pollChatFolderUpdates(folderId: Int32) -> Signal<Never, NoError> {
let signal = _internal_pollChatFolderUpdatesOnce(account: self.account, folderId: folderId)
return (
signal
|> then(
Signal<Never, NoError>.complete()
|> delay(10.0, queue: .concurrentDefaultQueue())
)
)
|> restart
}
public func subscribedChatFolderUpdates(folderId: Int32) -> Signal<ChatFolderUpdates?, NoError> {
return _internal_subscribedChatFolderUpdates(account: self.account, folderId: folderId)
}
public func joinAvailableChatsInFolder(updates: ChatFolderUpdates, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> {

View File

@ -370,7 +370,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
contentHeight += 14.0
var topBadge: String?
if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil {
if case .remove = component.subject {
} else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil {
topBadge = "+\(linkContents.peers.count)"
}

View File

@ -266,6 +266,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
}, openPasswordSetup: {
}, openPremiumIntro: {
}, openChatFolderUpdates: {
}, hideChatFolderUpdates: {
})
interaction.searchTextHighightState = searchQuery
self.interaction = interaction