diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index ced71d78bd..1f3500ef55 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9118,3 +9118,5 @@ Sorry for the inconvenience."; "Premium.MaxSharedFolderLinksFinalText" = "Sorry, you can only create **%1$@** invite links"; "Premium.GiftedTitle.Someone" = "Someone"; + +"Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)"; diff --git a/submodules/AccountContext/Sources/ChatListController.swift b/submodules/AccountContext/Sources/ChatListController.swift index 329f81d18e..a94a9f0f08 100644 --- a/submodules/AccountContext/Sources/ChatListController.swift +++ b/submodules/AccountContext/Sources/ChatListController.swift @@ -20,4 +20,6 @@ public protocol ChatListController: ViewController { func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) func playSignUpCompletedAnimation() + + func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d8840814b3..610a38efa6 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1546,7 +1546,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController for filter in filters { if filter.id == filterId, case let .filter(_, title, _, data) = filter { - if !data.includePeers.peers.isEmpty { + if !data.includePeers.peers.isEmpty && data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty { 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 @@ -2709,6 +2709,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) } + public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) { + let _ = (self.chatListDisplayNode.mainContainerNode.availableFiltersSignal + |> filter { filters in + return filters.contains(where: { item in + if case let .filter(filter) = item, filter.id == folderId { + return true + } else { + return false + } + }) + } + |> take(1) + |> map { _ -> Bool in + return true + } + |> timeout(1.0, queue: .mainQueue(), alternate: .single(false)) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + + if self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id != folderId { + self.chatListDisplayNode.mainContainerNode.switchToFilter(id: .filter(folderId), completion: { + completion() + }) + } else { + completion() + } + }) + } + private func askForFilterRemoval(id: Int32) { let apply: () -> Void = { [weak self] in guard let strongSelf = self else { @@ -2753,18 +2784,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .filter(_, title, _, data) = filter, data.isShared { let _ = (combineLatest( self.context.engine.data.get( - EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), + EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:))) ), self.context.engine.peers.getExportedChatFolderLinks(id: id), self.context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id) ) - |> deliverOnMainQueue).start(next: { [weak self] peers, links, defaultSelectedPeerIds in + |> deliverOnMainQueue).start(next: { [weak self] peerData, links, defaultSelectedPeerIds in guard let self else { return } let presentationData = self.presentationData + let peers = peerData.0 + + var memberCounts: [EnginePeer.Id: Int] = [:] + for (id, count) in peerData.1 { + if let count { + memberCounts[id] = count + } + } + var hasLinks = false if let links, !links.isEmpty { hasLinks = true @@ -2788,7 +2829,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } }, - alreadyMemberPeerIds: Set() + alreadyMemberPeerIds: Set(), + memberCounts: memberCounts ), completion: { [weak self] in guard let self else { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index cf8255ccb0..6e583b2866 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -513,7 +513,16 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:] private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)? - private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] + private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] { + didSet { + self.availableFiltersPromise.set(self.availableFilters) + } + } + private let availableFiltersPromise = ValuePromise<[ChatListContainerNodeFilter]>([.all], ignoreRepeated: true) + var availableFiltersSignal: Signal<[ChatListContainerNodeFilter], NoError> { + return self.availableFiltersPromise.get() + } + private var filtersLimit: Int32? = nil private var selectedId: ChatListFilterTabEntryId diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 1e9c0a0127..8841d18c50 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -31,10 +31,12 @@ private final class ChatListFilterPresetControllerArguments { let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void + let clearFocus: () -> Void let focusOnName: () -> Void let expandSection: (FilterSection) -> Void let createLink: () -> Void let openLink: (ExportedChatFolderLink) -> Void + let removeLink: (ExportedChatFolderLink) -> Void init( context: AccountContext, @@ -46,10 +48,12 @@ private final class ChatListFilterPresetControllerArguments { setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void, deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void, deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void, + clearFocus: @escaping () -> Void, focusOnName: @escaping () -> Void, expandSection: @escaping (FilterSection) -> Void, createLink: @escaping () -> Void, - openLink: @escaping (ExportedChatFolderLink) -> Void + openLink: @escaping (ExportedChatFolderLink) -> Void, + removeLink: @escaping (ExportedChatFolderLink) -> Void ) { self.context = context self.updateState = updateState @@ -60,10 +64,12 @@ private final class ChatListFilterPresetControllerArguments { self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions self.deleteIncludeCategory = deleteIncludeCategory self.deleteExcludeCategory = deleteExcludeCategory + self.clearFocus = clearFocus self.focusOnName = focusOnName self.expandSection = expandSection self.createLink = createLink self.openLink = openLink + self.removeLink = removeLink } } @@ -316,10 +322,10 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case excludePeerInfo(String) case includeExpand(String) case excludeExpand(String) - case inviteLinkHeader + case inviteLinkHeader(hasLinks: Bool) case inviteLinkCreate(hasLinks: Bool) case inviteLink(Int, ExportedChatFolderLink) - case inviteLinkInfo + case inviteLinkInfo(text: String) var section: ItemListSectionId { switch self { @@ -434,14 +440,16 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { case let .nameHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .name(placeholder, value): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .done, clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.name = value state.changedName = true return state } - }, action: {}, cleared: { + }, action: { + arguments.clearFocus() + }, cleared: { arguments.focusOnName() }) case .includePeersHeader(let text), .excludePeersHeader(let text): @@ -516,23 +524,23 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: { arguments.expandSection(.exclude) }) - case .inviteLinkHeader: + case let .inviteLinkHeader(hasLinks): //TODO:localize - return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: "SHARE FOLDER", badge: hasLinks ? nil : "NEW", sectionId: self.section) 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: { + let _ = hasLinks + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "Create an Invite Link", sectionId: self.section, editing: false, action: { arguments.createLink() }) case let .inviteLink(_, link): - return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in arguments.openLink(invite) - } contextAction: { invite, node, gesture in - //arguments.linkContextAction(invite, canEdit, node, gesture) - } - case .inviteLinkInfo: - //TODO:localize - return ItemListTextItem(presentationData: presentationData, text: .markdown("Share access to some of this folder's groups and channels with others."), sectionId: self.section) + }, removeAction: { invite in + arguments.removeLink(invite) + }, contextAction: nil) + case let .inviteLinkInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) } } } @@ -654,26 +662,25 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) } - if !isNewFilter { - entries.append(.inviteLinkHeader) - - 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) + var hasLinks = false + if let inviteLinks, !inviteLinks.isEmpty { + hasLinks = true } + entries.append(.inviteLinkHeader(hasLinks: hasLinks)) + entries.append(.inviteLinkCreate(hasLinks: hasLinks)) + + if let inviteLinks { + var index = 0 + for link in inviteLinks { + entries.append(.inviteLink(index, link)) + index += 1 + } + } + + //TODO:localize + entries.append(.inviteLinkInfo(text: hasLinks ? "Create more links to set up different access levels for different people." : "Share access to some of this folder's groups and channels with others.")) + return entries } @@ -1072,6 +1079,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var focusOnNameImpl: (() -> Void)? + var clearFocusImpl: (() -> Void)? var applyImpl: ((@escaping () -> Void) -> Void)? let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil) @@ -1270,6 +1278,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat return state } }, + clearFocus: { + clearFocusImpl?() + }, focusOnName: { focusOnNameImpl?() }, @@ -1281,44 +1292,62 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } }, createLink: { - applyImpl?({ - let state = stateValue.with({ $0 }) - - 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 - } + if currentPreset == nil { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let text = "Please finish creating this folder to share it." + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } else { + applyImpl?({ + let state = stateValue.with({ $0 }) - openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, 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 currentPreset, let data = currentPreset.data { + //TODO:localize + var unavailableText: String? + if !data.categories.isEmpty { + unavailableText = "You can’t share folders with include chat types." + } else if data.excludeArchived || data.excludeRead || data.excludeMuted { + unavailableText = "You can only share folders without chat types and excluded chats." + } else if !data.excludePeers.isEmpty { + unavailableText = "You can’t share folders with excluded chats" + } + 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) - if let updatedLink { - if let index = links.firstIndex(where: { $0.link == updatedLink.link }) { - links[index] = updatedLink - } else { - links.insert(updatedLink, at: 0) + return + } + + var previousLink: ExportedChatFolderLink? + openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in + pushControllerImpl?(c) + }, presentController: { c in + presentControllerImpl?(c, nil) + }, linkUpdated: { updatedLink in + let previousLinkValue = previousLink + previousLink = updatedLink + + 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 == updatedLink.link }) { + links[index] = updatedLink + } else { + links.insert(updatedLink, at: 0) + } + } else if let previousLinkValue { + if let index = links.firstIndex(where: { $0.link == previousLinkValue.link }) { + links.remove(at: index) + } } sharedLinks.set(.single(links)) - } + }) }) - }) - } - }) + } + }) + } }, openLink: { link in if let currentPreset, let _ = currentPreset.data { applyImpl?({ @@ -1331,13 +1360,13 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } if let updatedLink { - if let index = links.firstIndex(where: { $0 == link }) { + if let index = links.firstIndex(where: { $0.link == link.link }) { links.remove(at: index) } links.insert(updatedLink, at: 0) sharedLinks.set(.single(links)) } else { - if let index = links.firstIndex(where: { $0 == link }) { + if let index = links.firstIndex(where: { $0.link == link.link }) { links.remove(at: index) sharedLinks.set(.single(links)) } @@ -1347,6 +1376,22 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat })) }) } + }, + removeLink: { link in + if let currentPreset { + let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in + guard var links else { + return + } + + if let index = links.firstIndex(where: { $0.link == link.link }) { + links.remove(at: index) + } + sharedLinks.set(.single(links)) + + actionsDisposable.add(context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: link).start()) + }) + } } ) @@ -1462,6 +1507,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } } } + clearFocusImpl = { [weak controller] in + guard let controller = controller else { + return + } + controller.view.endEditing(true) + } controller.attemptNavigation = { _ in return attemptNavigationImpl?() ?? true } @@ -1493,7 +1544,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat return false } } else { - if state.isComplete { + if currentPreset != nil, state.isComplete { displaySaveAlert() return false } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index b1ebb7e95c..52eff9e601 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -393,14 +393,23 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch if case let .filter(_, title, _, data) = filter, data.isShared { let _ = (combineLatest( context.engine.data.get( - EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), + EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:))) ), context.engine.peers.getExportedChatFolderLinks(id: id), context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id) ) - |> deliverOnMainQueue).start(next: { peers, links, defaultSelectedPeerIds in + |> deliverOnMainQueue).start(next: { peerData, links, defaultSelectedPeerIds in let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let peers = peerData.0 + var memberCounts: [EnginePeer.Id: Int] = [:] + for (id, count) in peerData.1 { + if let count { + memberCounts[id] = count + } + } + var hasLinks = false if let links, !links.isEmpty { hasLinks = true @@ -420,7 +429,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch return false } }, - alreadyMemberPeerIds: Set() + alreadyMemberPeerIds: Set(), + memberCounts: memberCounts ) ) pushControllerImpl?(previewScreen) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 8748e74d1e..2050206093 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -215,7 +215,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } else { updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) } - updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Share"), color: item.presentationData.theme.list.disclosureArrowColor) + updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor) } let peerRevealOptions: [ItemListRevealOption] diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 401441ab78..26d1421dd8 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -69,7 +69,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool) case peersHeader(String) - case peer(index: Int, peer: EnginePeer, isSelected: Bool, isEnabled: Bool) + case peer(index: Int, peer: EnginePeer, isSelected: Bool, disabledReasonText: String?) case peersInfo(String) var section: ItemListSectionId { @@ -149,8 +149,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry { } else { return false } - case let .peer(index, peer, isSelected, isEnabled): - if case .peer(index, peer, isSelected, isEnabled) = rhs { + case let .peer(index, peer, isSelected, disabledReasonText): + if case .peer(index, peer, isSelected, disabledReasonText) = rhs { return true } else { return false @@ -195,7 +195,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peersInfo(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) - case let .peer(_, peer, isSelected, isEnabled): + case let .peer(_, peer, isSelected, disabledReasonText): //TODO:localize return ItemListPeerItem( presentationData: presentationData, @@ -204,16 +204,16 @@ private enum InviteLinksListEntry: ItemListNodeEntry { context: arguments.context, peer: peer, presence: nil, - text: .text(isEnabled ? "you can invite others here" : "you can't invite others here", .secondary), + text: .text(disabledReasonText ?? "you can invite others here", .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), - switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled), + switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: disabledReasonText == nil), enabled: true, selectable: true, highlightable: false, sectionId: self.section, action: { - arguments.peerAction(peer, isEnabled) + arguments.peerAction(peer, disabledReasonText == nil) }, setPeerIdWithRevealedOptions: { _, _ in }, @@ -274,8 +274,19 @@ private func folderInviteLinkListControllerEntries( } for peer in sortedPeers { - let isEnabled = canShareLinkToPeer(peer: peer) - entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled)) + var disabledReasonText: String? + if !canShareLinkToPeer(peer: peer) { + if case let .user(user) = peer { + if user.botInfo != nil { + disabledReasonText = "you can't share chats with bots" + } else { + disabledReasonText = "you can't share private chats" + } + } else { + disabledReasonText = "you can't invite other here" + } + } + entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), disabledReasonText: disabledReasonText)) } if let infoString { @@ -391,7 +402,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese let state = stateValue.with({ $0 }) - let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Link title", value: state.title ?? "", apply: { value in + let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Name This Link", titleFont: .bold, value: state.title ?? "", apply: { value in if let value { updateState { state in var state = state @@ -458,7 +469,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese state.selectedPeerIds.remove(peer.id) } else { state.selectedPeerIds.insert(peer.id) - added = true + + if let currentInvitation, !currentInvitation.peerIds.contains(peer.id) { + added = true + } } return state @@ -469,7 +483,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese dismissTooltipsImpl?() //TODO:localize - displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.")) + displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.", timeout: 8)) } } else { //TODO:localize @@ -549,7 +563,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese 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) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred.", timeout: nil), 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?() @@ -587,6 +601,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } }) + let previousState = Atomic(value: nil) + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest(queue: .mainQueue(), presentationData, @@ -595,7 +611,13 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese ) |> map { presentationData, state, allPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in let crossfade = false - let animateChanges = false + + var animateChanges = false + + let previousStateValue = previousState.swap(state) + if let previousStateValue, previousStateValue.selectedPeerIds != state.selectedPeerIds { + animateChanges = true + } //TODO:localize let title: ItemListControllerTitle @@ -610,7 +632,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese 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: { + doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty, action: { applyChangesImpl?() }) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 4bf2cbcbf4..794cb8df90 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -66,7 +66,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { case requestHeader(PresentationTheme, String, String, Bool) case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case importerHeader(PresentationTheme, String, String, Bool) - case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) + case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool) var stableId: InviteLinkViewEntryId { switch self { @@ -82,7 +82,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { return .request(peer.id) case .importerHeader: return .importerHeader - case let .importer(_, _, _, peer, _, _): + case let .importer(_, _, _, peer, _, _, _): return .importer(peer.id) } } @@ -125,8 +125,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } - case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading): - if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsLoading == rhsLoading { + case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading): + if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading { return true } else { return false @@ -180,11 +180,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { case .importer: return true } - case let .importer(lhsIndex, _, _, _, _, _): + case let .importer(lhsIndex, _, _, _, _, _, _): switch rhs { case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: return false - case let .importer(rhsIndex, _, _, _, _, _): + case let .importer(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -224,7 +224,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { additionalText = .none } return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText) - case let .importer(_, _, dateTimeFormat, peer, date, loading), let .request(_, _, dateTimeFormat, peer, date, loading): + case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading): + let dateString: String + if joinedViaFolderLink { + //TODO:localize + dateString = "joined via a folder invite link" + } else { + dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) + } + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + interaction.openPeer(peer.id) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + case let .request(_, _, dateTimeFormat, peer, date, loading): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) @@ -753,14 +764,14 @@ public final class InviteLinkViewController: ViewController { loading = true let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: []) for i in 0 ..< count { - entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true)) + entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true)) } } else { count = min(4, Int32(state.importers.count)) loading = false for importer in state.importers { if let peer = importer.peer.peer { - entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, false)) + entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false)) } index += 1 } diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift index 0547c76ede..5087005653 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift @@ -38,6 +38,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let style: ItemListStyle let tapAction: ((ExportedChatFolderLink) -> Void)? + let removeAction: ((ExportedChatFolderLink) -> Void)? let contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)? public let tag: ItemListItemTag? @@ -48,6 +49,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem { sectionId: ItemListSectionId, style: ItemListStyle, tapAction: ((ExportedChatFolderLink) -> Void)?, + removeAction: ((ExportedChatFolderLink) -> Void)?, contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil ) { @@ -57,6 +59,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.style = style self.tapAction = tapAction + self.removeAction = removeAction self.contextAction = contextAction self.tag = tag } @@ -125,7 +128,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem { } } -public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListItemNode { +public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -506,6 +509,15 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte strongSelf.placeholderNode = nil shimmerNode.removeFromSupernode() } + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + if item.removeAction != nil { + //TODO:localize + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: "Delete", icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + } else { + strongSelf.setRevealOptions((left: [], right: [])) + } } }) } @@ -565,6 +577,21 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte shimmerNode.updateAbsoluteRect(rect, within: containerSize) } } + + override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + transition.updateSublayerTransformOffset(layer: self.containerNode.layer, offset: CGPoint(x: offset, y: 0.0)) + } + + override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + if let item = self.layoutParams?.0, let invite = item.invite { + item.removeAction?(invite) + } + + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } } private struct ContentParticle { diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index beaee162e1..85197ec7af 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -541,7 +541,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co let data = try Data(contentsOf: url) if data.count > settings.maxSize { - presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)) + presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil)) souceUrl.stopAccessingSecurityScopedResource() TempBox.shared.dispose(tempFile) @@ -594,7 +594,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co if duration > Double(settings.maxDuration) { souceUrl.stopAccessingSecurityScopedResource() - presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) + presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil)) } else { Logger.shared.log("NotificationSoundSelection", "Uploading sound") diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index 2b6dcbf2d8..eae781c4db 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -150,6 +150,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg private final class PromptAlertContentNode: AlertContentNode { private let strings: PresentationStrings private let text: String + private let titleFont: PromptControllerTitleFont private let textNode: ASTextNode let inputFieldNode: PromptInputFieldNode @@ -174,9 +175,10 @@ private final class PromptAlertContentNode: AlertContentNode { return self.isUserInteractionEnabled } - init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String?) { + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, value: String?) { self.strings = strings self.text = text + self.titleFont = titleFont self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 2 @@ -244,7 +246,14 @@ private final class PromptAlertContentNode: AlertContentNode { } override func updateTheme(_ theme: AlertControllerTheme) { - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + let titleFontValue: UIFont + switch self.titleFont { + case .regular: + titleFontValue = Font.regular(13.0) + case .bold: + titleFontValue = Font.semibold(17.0) + } + self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center) self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -379,7 +388,12 @@ private final class PromptAlertContentNode: AlertContentNode { } } -public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, value: String?, apply: @escaping (String?) -> Void) -> AlertController { +public enum PromptControllerTitleFont { + case regular + case bold +} + +public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, value: String?, apply: @escaping (String?) -> Void) -> AlertController { let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } var dismissImpl: ((Bool) -> Void)? @@ -393,7 +407,7 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent applyImpl?() })] - let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, value: value) + let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, value: value) contentNode.complete = { applyImpl?() } diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 24d923b163..1cadfcf3c5 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -238,6 +238,10 @@ public final class QrCodeScreen: ViewController { case let .invite(_, isGroup): title = self.presentationData.strings.InviteLink_QRCode_Title text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel + case .chatFolder: + //TODO:localize + title = "Invite by QR Code" + text = "Everyone on Telegram can scan this code to join your folder." default: title = "" text = "" diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index 7f0f102541..7b2934724c 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -470,7 +470,7 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat let presentGlobalController = context.sharedContext.presentGlobalController let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: false).start(completed: { Queue.mainQueue().after(0.1) { - presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) + presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil) } }) }) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index baa60b8a18..0b2baf2d02 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -920,7 +920,7 @@ public func privacyAndSecurityController( hapticFeedback.impact() var alreadyPresented = false - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip), elevatedLayout: false, animateInAsReplacement: false, action: { action in + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in if action == .info { if !alreadyPresented { let controller = PremiumIntroScreen(context: context, source: .settings) diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 7b0864eb3b..89e9e5bbd8 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -1487,7 +1487,7 @@ public final class SolidRoundedButtonView: UIView { badgeNode = current } else { badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor) - badgeNode.alpha = self.titleNode.alpha + badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0 self.badgeNode = badgeNode self.addSubnode(badgeNode) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0fcf2120c3..2242a6929a 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -118,7 +118,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1091179342] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteRevoke($0) } dict[-484690728] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantInvite($0) } dict[405815507] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoin($0) } - dict[1557846647] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) } + dict[-23084712] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) } dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) } dict[-124291086] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantLeave($0) } dict[-115071790] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantMute($0) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 52844c2fb7..d64f7a61e9 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -700,7 +700,7 @@ public extension Api { case channelAdminLogEventActionExportedInviteRevoke(invite: Api.ExportedChatInvite) case channelAdminLogEventActionParticipantInvite(participant: Api.ChannelParticipant) case channelAdminLogEventActionParticipantJoin - case channelAdminLogEventActionParticipantJoinByInvite(invite: Api.ExportedChatInvite) + case channelAdminLogEventActionParticipantJoinByInvite(flags: Int32, invite: Api.ExportedChatInvite) case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64) case channelAdminLogEventActionParticipantLeave case channelAdminLogEventActionParticipantMute(participant: Api.GroupCallParticipant) @@ -878,10 +878,11 @@ public extension Api { } break - case .channelAdminLogEventActionParticipantJoinByInvite(let invite): + case .channelAdminLogEventActionParticipantJoinByInvite(let flags, let invite): if boxed { - buffer.appendInt32(1557846647) + buffer.appendInt32(-23084712) } + serializeInt32(flags, buffer: buffer, boxed: false) invite.serialize(buffer, true) break case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): @@ -1059,8 +1060,8 @@ public extension Api { return ("channelAdminLogEventActionParticipantInvite", [("participant", participant as Any)]) case .channelAdminLogEventActionParticipantJoin: return ("channelAdminLogEventActionParticipantJoin", []) - case .channelAdminLogEventActionParticipantJoinByInvite(let invite): - return ("channelAdminLogEventActionParticipantJoinByInvite", [("invite", invite as Any)]) + case .channelAdminLogEventActionParticipantJoinByInvite(let flags, let invite): + return ("channelAdminLogEventActionParticipantJoinByInvite", [("flags", flags as Any), ("invite", invite as Any)]) case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", invite as Any), ("approvedBy", approvedBy as Any)]) case .channelAdminLogEventActionParticipantLeave: @@ -1431,13 +1432,16 @@ public extension Api { return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoin } public static func parse_channelAdminLogEventActionParticipantJoinByInvite(_ reader: BufferReader) -> ChannelAdminLogEventAction? { - var _1: Api.ExportedChatInvite? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.ExportedChatInvite? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite + _2 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite } let _c1 = _1 != nil - if _c1 { - return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByInvite(invite: _1!) + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByInvite(flags: _1!, invite: _2!) } else { return nil diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 11c04cf1d8..ccd9a5bfee 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1574,7 +1574,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController }).start() } - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess, timeout: nil), action: { _ in return false }) } }) self?.controller?.present(controller, in: .window(.root)) @@ -1595,7 +1595,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController if let strongSelf = self, let (firstName, lastName) = firstAndLastName { let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start() - strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess, timeout: nil), action: { _ in return false }) } }) self?.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 523ad9a3e9..481d2b9984 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -73,7 +73,7 @@ public enum AdminLogEventAction { case deleteExportedInvitation(ExportedInvitation) case revokeExportedInvitation(ExportedInvitation) case editExportedInvitation(previous: ExportedInvitation, updated: ExportedInvitation) - case participantJoinedViaInvite(ExportedInvitation) + case participantJoinedViaInvite(invitation: ExportedInvitation, joinedViaFolderLink: Bool) case changeHistoryTTL(previousValue: Int32?, updatedValue: Int32?) case changeTheme(previous: String?, updated: String?) case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId) @@ -268,8 +268,8 @@ func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, m action = .revokeExportedInvitation(ExportedInvitation(apiExportedInvite: invite)) case let .channelAdminLogEventActionExportedInviteEdit(prevInvite, newInvite): action = .editExportedInvitation(previous: ExportedInvitation(apiExportedInvite: prevInvite), updated: ExportedInvitation(apiExportedInvite: newInvite)) - case let .channelAdminLogEventActionParticipantJoinByInvite(invite): - action = .participantJoinedViaInvite(ExportedInvitation(apiExportedInvite: invite)) + case let .channelAdminLogEventActionParticipantJoinByInvite(flags, invite): + action = .participantJoinedViaInvite(invitation: ExportedInvitation(apiExportedInvite: invite), joinedViaFolderLink: (flags & (1 << 0)) != 0) case let .channelAdminLogEventActionParticipantVolume(participant): let parsedParticipant = GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate(participant) action = .groupCallUpdateParticipantVolume(peerId: parsedParticipant.peerId, volume: parsedParticipant.volume ?? 10000) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 62ec0338ff..93b0470a1c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -389,30 +389,46 @@ extension ChatListFilter { case .allChats: return nil case let .filter(id, title, emoticon, data): - var flags: Int32 = 0 - if data.excludeMuted { - flags |= 1 << 11 - } - if data.excludeRead { - flags |= 1 << 12 - } - if data.excludeArchived { - flags |= 1 << 13 - } - flags |= data.categories.apiFlags - if emoticon != nil { - flags |= 1 << 25 - } - return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in - if data.includePeers.pinnedPeers.contains(peerId) { - return nil + if data.isShared { + var flags: Int32 = 0 + if emoticon != nil { + flags |= 1 << 25 } - return transaction.getPeer(peerId).flatMap(apiInputPeer) - }, excludePeers: data.excludePeers.compactMap { peerId -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - }) + + return .dialogFilterCommunity(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in + if data.includePeers.pinnedPeers.contains(peerId) { + return nil + } + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }) + } else { + var flags: Int32 = 0 + if data.excludeMuted { + flags |= 1 << 11 + } + if data.excludeRead { + flags |= 1 << 12 + } + if data.excludeArchived { + flags |= 1 << 13 + } + flags |= data.categories.apiFlags + if emoticon != nil { + flags |= 1 << 25 + } + return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in + if data.includePeers.pinnedPeers.contains(peerId) { + return nil + } + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }, excludePeers: data.excludePeers.compactMap { peerId -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }) + } } } } @@ -873,14 +889,21 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, struct ChatListFiltersState: Codable, Equatable { struct ChatListFilterUpdates: Codable, Equatable { + struct MemberCount: Codable, Equatable { + var id: PeerId + var count: Int32 + } + var folderId: Int32 var timestamp: Int32 var peerIds: [PeerId] + var memberCounts: [MemberCount] - init(folderId: Int32, timestamp: Int32, peerIds: [PeerId]) { + init(folderId: Int32, timestamp: Int32, peerIds: [PeerId], memberCounts: [MemberCount]) { self.folderId = folderId self.timestamp = timestamp self.peerIds = peerIds + self.memberCounts = memberCounts } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 3e86bc6bae..cfb408eca0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -7,7 +7,7 @@ public func canShareLinkToPeer(peer: EnginePeer) -> Bool { var isEnabled = false switch peer { case let .channel(channel): - if channel.adminRights != nil && channel.hasPermission(.inviteMembers) { + if channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { isEnabled = true } else if channel.username != nil { isEnabled = true @@ -237,17 +237,20 @@ public final class ChatFolderLinkContents { public let title: String? public let peers: [EnginePeer] public let alreadyMemberPeerIds: Set + public let memberCounts: [EnginePeer.Id: Int] public init( localFilterId: Int32?, title: String?, peers: [EnginePeer], - alreadyMemberPeerIds: Set + alreadyMemberPeerIds: Set, + memberCounts: [EnginePeer.Id: Int] ) { self.localFilterId = localFilterId self.title = title self.peers = peers self.alreadyMemberPeerIds = alreadyMemberPeerIds + self.memberCounts = memberCounts } } @@ -264,6 +267,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal Peer in @@ -294,12 +303,13 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal Peer in @@ -356,7 +371,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal castError(CheckChatFolderLinkError.self) @@ -370,12 +385,31 @@ public enum JoinChatFolderLinkError { case tooManyChannels(limit: Int32, premiumLimit: Int32) } -func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal { - return account.postbox.transaction { transaction -> [Api.InputPeer] in - return peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer) +public final class JoinChatFolderResult { + public let folderId: Int32 + public let title: String + public let newChatCount: Int + + public init(folderId: Int32, title: String, newChatCount: Int) { + self.folderId = folderId + self.title = title + self.newChatCount = newChatCount + } +} + +func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal { + return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in + var newChatCount = 0 + for peerId in peerIds { + if transaction.getPeerChatListIndex(peerId) != nil { + newChatCount += 1 + } + } + + return (peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer), newChatCount) } |> castError(JoinChatFolderLinkError.self) - |> mapToSignal { inputPeers -> Signal in + |> mapToSignal { inputPeers, newChatCount -> Signal in return account.network.request(Api.functions.communities.joinCommunityInvite(slug: slug, peers: inputPeers)) |> `catch` { error -> Signal in if error.errorDescription == "USER_CHANNELS_TOO_MUCH" { @@ -427,10 +461,36 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi return .fail(.generic) } } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in account.stateManager.addUpdates(result) - return .complete() + var folderResult: JoinChatFolderResult? + for update in result.allUpdates { + if case let .updateDialogFilter(_, id, data) = update { + if let data = data, case let .filter(_, title, _, _) = ChatListFilter(apiFilter: data) { + folderResult = JoinChatFolderResult(folderId: id, title: title, newChatCount: newChatCount) + } + break + } + } + + if let folderResult = folderResult { + return _internal_updatedChatListFilters(postbox: account.postbox) + |> castError(JoinChatFolderLinkError.self) + |> filter { filters -> Bool in + if filters.contains(where: { $0.id == folderResult.folderId }) { + return true + } else { + return false + } + } + |> take(1) + |> map { _ -> JoinChatFolderResult in + return folderResult + } + } else { + return .fail(.generic) + } } } } @@ -439,23 +499,26 @@ public final class ChatFolderUpdates: Equatable { fileprivate let folderId: Int32 fileprivate let title: String fileprivate let missingPeers: [EnginePeer] + fileprivate let memberCounts: [EnginePeer.Id: Int] public var availableChatsToJoin: Int { return self.missingPeers.count } public var chatFolderLinkContents: ChatFolderLinkContents { - return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set()) + return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set(), memberCounts: self.memberCounts) } fileprivate init( folderId: Int32, title: String, - missingPeers: [EnginePeer] + missingPeers: [EnginePeer], + memberCounts: [EnginePeer.Id: Int] ) { self.folderId = folderId self.title = title self.missingPeers = missingPeers + self.memberCounts = memberCounts } public static func ==(lhs: ChatFolderUpdates, rhs: ChatFolderUpdates) -> Bool { @@ -513,7 +576,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S var state = state state.updates.removeAll(where: { $0.folderId == folderId }) - state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: [])) + state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: [], memberCounts: [])) return state }) @@ -525,6 +588,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S return account.postbox.transaction { transaction -> Void in var peers: [Peer] = [] var peerPresences: [PeerId: Api.User] = [:] + var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = [] for user in users { let telegramUser = TelegramUser(user: user) @@ -535,6 +599,11 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S if let peer = parseTelegramGroupOrChannel(chat: chat) { peers.append(peer) } + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat { + if let participantsCount = participantsCount { + memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount)) + } + } } updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in @@ -546,7 +615,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S 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))) + state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId), memberCounts: memberCounts)) return state }) @@ -561,6 +630,7 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> struct InternalData: Equatable { var title: String var peerIds: [EnginePeer.Id] + var memberCounts: [EnginePeer.Id: Int] } return _internal_updatedChatListFiltersState(postbox: account.postbox) @@ -575,7 +645,11 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> return nil } let filteredPeerIds: [PeerId] = update.peerIds.filter { !data.includePeers.peers.contains($0) } - return InternalData(title: title, peerIds: filteredPeerIds) + var memberCounts: [PeerId: Int] = [:] + for item in update.memberCounts { + memberCounts[item.id] = Int(item.count) + } + return InternalData(title: title, peerIds: filteredPeerIds, memberCounts: memberCounts) } |> distinctUntilChanged |> mapToSignal { internalData -> Signal in @@ -592,7 +666,7 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) -> peers.append(EnginePeer(peer)) } } - return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers) + return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers, memberCounts: internalData.memberCounts) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index d90c0f814b..5a2ebcea7d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -664,6 +664,7 @@ public struct PeerInvitationImportersState: Equatable { public var date: Int32 public var about: String? public var approvedBy: PeerId? + public var joinedViaFolderLink: Bool } public var importers: [Importer] public var isLoadingMore: Bool @@ -708,6 +709,7 @@ final class CachedPeerInvitationImporters: Codable { let peerIds: [PeerId] let dates: [PeerId: Int32] let abouts: [PeerId: String] + let joinedViaFolderLink: [PeerId: Bool] let count: Int32 static func key(peerId: PeerId, link: String, requested: Bool) -> ValueBoxKey { @@ -727,13 +729,17 @@ final class CachedPeerInvitationImporters: Codable { $0[$1.peer.peerId] = about } } + self.joinedViaFolderLink = importers.reduce(into: [PeerId: Bool]()) { + $0[$1.peer.peerId] = $1.joinedViaFolderLink + } self.count = count } - init(peerIds: [PeerId], dates: [PeerId: Int32], abouts: [PeerId: String], count: Int32) { + init(peerIds: [PeerId], dates: [PeerId: Int32], abouts: [PeerId: String], joinedViaFolderLink: [PeerId: Bool], count: Int32) { self.peerIds = peerIds self.dates = dates self.abouts = abouts + self.joinedViaFolderLink = joinedViaFolderLink self.count = count } @@ -752,6 +758,16 @@ final class CachedPeerInvitationImporters: Codable { } self.dates = dates + var joinedViaFolderLink: [PeerId: Bool] = [:] + let joinedViaFolderLinkArray = try container.decode([Int64].self, forKey: "joinedViaFolderLink") + for index in stride(from: 0, to: joinedViaFolderLinkArray.endIndex, by: 2) { + let userId = datesArray[index] + let value = datesArray[index + 1] + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + joinedViaFolderLink[peerId] = value != 0 + } + self.joinedViaFolderLink = joinedViaFolderLink + var abouts: [PeerId: String] = [:] let aboutsArray = try container.decodeIfPresent([DictionaryPair].self, forKey: "abouts") if let aboutsArray = aboutsArray { @@ -777,6 +793,13 @@ final class CachedPeerInvitationImporters: Codable { } try container.encode(dates, forKey: "dates") + var joinedViaFolderLink: [Int64] = [] + for (peerId, value) in self.joinedViaFolderLink { + joinedViaFolderLink.append(peerId.id._internalGetInt64Value()) + joinedViaFolderLink.append(Int64(value ? 1 : 0)) + } + try container.encode(joinedViaFolderLink, forKey: "joinedViaFolderLink") + var abouts: [DictionaryPair] = [] for (peerId, about) in self.abouts { abouts.append(DictionaryPair(peerId.id._internalGetInt64Value(), value: about)) @@ -847,7 +870,7 @@ private final class PeerInvitationImportersContextImpl { var result: [PeerInvitationImportersState.Importer] = [] for peerId in cachedResult.peerIds { if let peer = transaction.getPeer(peerId), let date = cachedResult.dates[peerId] { - result.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: cachedResult.abouts[peerId])) + result.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: cachedResult.abouts[peerId], joinedViaFolderLink: cachedResult.joinedViaFolderLink[peerId] ?? false)) } else { return nil } @@ -958,15 +981,17 @@ private final class PeerInvitationImportersContextImpl { let date: Int32 let about: String? let approvedBy: PeerId? + let joinedViaFolderLink: Bool switch importer { - case let .chatInviteImporter(_, userId, dateValue, aboutValue, approvedByValue): + case let .chatInviteImporter(flags, userId, dateValue, aboutValue, approvedByValue): + joinedViaFolderLink = (flags & (1 << 3)) != 0 peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) date = dateValue about = aboutValue approvedBy = approvedByValue.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } } if let peer = transaction.getPeer(peerId) { - resultImporters.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: about, approvedBy: approvedBy)) + resultImporters.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: about, approvedBy: approvedBy, joinedViaFolderLink: joinedViaFolderLink)) } } if populateCache && query == nil { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index a393906ab5..0b9c8f9286 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1054,7 +1054,7 @@ public extension TelegramEngine { return _internal_checkChatFolderLink(account: self.account, slug: slug) } - public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal { + public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal { return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds) } diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 64f8a64554..b8ed4317c6 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -55,12 +55,14 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { var containerInset: CGFloat var bottomInset: CGFloat var topInset: CGFloat + var contentHeight: CGFloat - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, contentHeight: CGFloat) { self.containerSize = containerSize self.containerInset = containerInset self.bottomInset = bottomInset self.topInset = topInset + self.contentHeight = contentHeight } } @@ -82,6 +84,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { private let scrollView: ScrollView private let scrollContentClippingView: SparseContainerView private let scrollContentView: UIView + private let bottomBackgroundLayer: SimpleLayer + private let bottomSeparatorLayer: SimpleLayer private let topIcon = ComponentView() @@ -134,6 +138,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.itemContainerView.clipsToBounds = true self.itemContainerView.layer.cornerRadius = 10.0 + self.bottomBackgroundLayer = SimpleLayer() + self.bottomSeparatorLayer = SimpleLayer() + super.init(frame: frame) self.addSubview(self.dimView) @@ -165,6 +172,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.scrollContentView.addSubview(self.itemContainerView) + self.layer.addSublayer(self.bottomBackgroundLayer) + self.layer.addSublayer(self.bottomSeparatorLayer) + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } @@ -231,6 +241,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { topOffset = max(0.0, topOffset) transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + let bottomDistance = itemLayout.contentHeight - self.scrollView.bounds.maxY + let bottomAlphaDistance: CGFloat = 30.0 + var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance + bottomAlpha = max(0.0, min(1.0, bottomAlpha)) + + let bottomOverlayAlpha: CGFloat = bottomAlpha + transition.setAlpha(layer: self.bottomBackgroundLayer, alpha: bottomOverlayAlpha) + transition.setAlpha(layer: self.bottomSeparatorLayer, alpha: bottomOverlayAlpha) + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) @@ -244,10 +263,12 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { func animateIn() { self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.backgroundLayer.frame.minY + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.bottomBackgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.bottomSeparatorLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } @@ -265,6 +286,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { completion() }) self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomBackgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomSeparatorLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) @@ -308,6 +331,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor + self.bottomBackgroundLayer.backgroundColor = environment.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor } transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -485,6 +510,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.items[id] = item } + var subtitle: String? + if linkContents.alreadyMemberPeerIds.contains(peer.id) { + subtitle = "You are already a member" + } else if let memberCount = linkContents.memberCounts[peer.id] { + subtitle = "\(memberCount) participants" + } + let itemSize = item.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( @@ -494,7 +526,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { sideInset: 0.0, title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), peer: peer, - subtitle: nil, + subtitle: subtitle, 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 @@ -647,7 +679,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { self.selectedItems.insert(peerId) } } - self.state?.updated(transition: .immediate) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) } )), environment: {}, @@ -737,21 +769,72 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { controller.dismiss() } else if let _ = component.linkContents { if self.joinDisposable == nil, !self.selectedItems.isEmpty { - let joinSignal: Signal + let joinSignal: Signal switch component.subject { case .remove: return case let .slug(slug): joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems)) + |> map(Optional.init) case let .updates(updates): + var result: JoinChatFolderResult? + if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title { + result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: self.selectedItems.count) + } joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems)) + |> map { _ -> JoinChatFolderResult? in + } + |> then(Signal.single(result)) } self.inProgress = true self.state?.updated(transition: .immediate) self.joinDisposable = (joinSignal - |> deliverOnMainQueue).start(error: { [weak self] error in + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + if let result, let navigationController = controller.navigationController as? NavigationController { + var chatListController: ChatListController? + for viewController in navigationController.viewControllers { + if let rootController = viewController as? TabBarController { + for c in rootController.controllers { + if let c = c as? ChatListController { + chatListController = c + break + } + } + } else if let c = viewController as? ChatListController { + chatListController = c + break + } + } + + if let chatListController { + navigationController.popToRoot(animated: true) + let context = component.context + chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in + guard let context, let chatListController else { + return + } + + //TODO:localize + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + if case .updates = component.subject { + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Updated", text: "You have joined \(result.newChatCount) new chats", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current) + } else if result.newChatCount != 0 { + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Added", text: "You also joined \(result.newChatCount) chats", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current) + } else { + chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Folder \(result.title) Added", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current) + } + }) + } + } + + controller.dismiss() + }, error: { [weak self] error in guard let self, let component = self.component, let controller = self.environment?.controller() else { return } @@ -772,11 +855,6 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { controller.push(limitController) controller.dismiss() } - }, completed: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() }) } else { controller.dismiss() @@ -796,6 +874,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } + transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight))) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + 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), @@ -817,16 +898,16 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset) - self.scrollContentClippingView.layer.cornerRadius = 10.0 + //self.scrollContentClippingView.layer.cornerRadius = 10.0 - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset + 56.0))) + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 8.0 - (containerInset + 56.0))) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) @@ -876,6 +957,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true self.automaticallyControlPresentationContextLayout = false + self.lockOrientation = true } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift index eb72a2a56e..1ee2e163ef 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift @@ -182,7 +182,7 @@ final class PeerListItemComponent: Component { if themeUpdated { var theme = CheckNodeTheme(theme: component.theme, style: .plain) if isTinted { - theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35) + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) } checkLayer.theme = theme } @@ -190,7 +190,7 @@ final class PeerListItemComponent: Component { } else { var theme = CheckNodeTheme(theme: component.theme, style: .plain) if isTinted { - theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35) + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) } checkLayer = CheckLayer(theme: theme) self.checkLayer = checkLayer diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/SharedFolderListIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/SharedFolderListIcon.imageset/Contents.json new file mode 100644 index 0000000000..5cc301d9a9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/SharedFolderListIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "link_18.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/SharedFolderListIcon.imageset/link_18.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/SharedFolderListIcon.imageset/link_18.pdf new file mode 100644 index 0000000000..1c35657b55 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/SharedFolderListIcon.imageset/link_18.pdf @@ -0,0 +1,101 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.304932 1.549805 cm +0.000000 0.000000 0.000000 scn +7.766377 12.077935 m +8.749319 13.060877 10.342982 13.060877 11.325925 12.077935 c +12.308867 11.094994 12.308867 9.501329 11.325925 8.518387 c +9.825925 7.018387 l +8.842983 6.035446 7.249319 6.035446 6.266377 7.018387 c +6.135469 7.149295 6.022435 7.290437 5.926912 7.438976 c +5.728258 7.747883 5.316799 7.837261 5.007892 7.638608 c +4.698985 7.439953 4.609607 7.028494 4.808261 6.719588 c +4.954801 6.491719 5.127476 6.276385 5.325925 6.077936 c +6.828264 4.575597 9.264038 4.575597 10.766377 6.077936 c +12.266377 7.577936 l +13.768716 9.080275 13.768716 11.516048 12.266377 13.018387 c +10.764037 14.520726 8.328264 14.520726 6.825925 13.018387 c +5.325925 11.518387 l +5.066226 11.258689 5.066226 10.837634 5.325925 10.577935 c +5.585624 10.318236 6.006679 10.318236 6.266377 10.577935 c +7.766377 12.077935 l +h +5.626755 2.818445 m +4.643812 1.835504 3.050149 1.835504 2.067207 2.818445 c +1.084265 3.801387 1.084265 5.395051 2.067207 6.377992 c +3.567207 7.877992 l +4.550149 8.860934 6.143812 8.860934 7.126754 7.877992 c +7.257662 7.747084 7.370696 7.605942 7.466219 7.457404 c +7.664873 7.148497 8.076332 7.059119 8.385240 7.257772 c +8.694146 7.456426 8.783525 7.867885 8.584870 8.176792 c +8.438332 8.404661 8.265656 8.619995 8.067206 8.818444 c +6.564867 10.320784 4.129094 10.320784 2.626755 8.818444 c +1.126755 7.318444 l +-0.375585 5.816106 -0.375585 3.380332 1.126755 1.877994 c +2.629094 0.375654 5.064867 0.375654 6.567206 1.877994 c +8.067206 3.377994 l +8.326905 3.637691 8.326905 4.058746 8.067206 4.318445 c +7.807508 4.578144 7.386453 4.578144 7.126754 4.318445 c +5.626755 2.818445 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1707 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 18.000000 18.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001797 00000 n +0000001820 00000 n +0000001993 00000 n +0000002067 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2126 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c64bdf6ed3..6bedd7b82f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -7292,7 +7292,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, case let .message(index) = toIndex { if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { if messageId.peerId == index.id.peerId { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist), elevatedLayout: false, action: { _ in return true }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } } else if let controllerInteraction = strongSelf.controllerInteraction { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) { @@ -8771,12 +8771,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G bannedMediaInput = true } else if channel.hasBannedPermission(.banSendVoice) != nil { if !isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) return } } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { if isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) return } } @@ -8785,12 +8785,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G bannedMediaInput = true } else if group.hasBannedPermission(.banSendVoice) { if !isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) return } } else if group.hasBannedPermission(.banSendInstantVideos) { if isVideo { - strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText())) + strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil)) return } } @@ -10092,7 +10092,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable) }, presentGigagroupHelp: { [weak self] in if let strongSelf = self { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription), elevatedLayout: false, action: { _ in return true }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current) } }, editMessageMedia: { [weak self] messageId, draw in if let strongSelf = self { @@ -11166,7 +11166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: { let context = strongSelf.context @@ -12959,7 +12959,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } }), case let .app(_, botName, _) = button { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } else { let _ = (context.engine.messages.getAttachMenuBot(botId: botId) |> deliverOnMainQueue).start(next: { bot in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 56421f8581..cdb10a3a38 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -815,7 +815,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start() let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip) + let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip, timeout: nil) controllerInteraction.displayUndo(content) }), false), at: 0) actions.insert(.separator, at: 1) @@ -842,9 +842,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 })) if size > settings.maxSize { - controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)) + controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil)) } else if Double(duration) > Double(settings.maxDuration) { - controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string)) + controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil)) } else { let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file)) |> deliverOnMainQueue).start(completed: { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 3614aae7fb..18ac683337 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -164,7 +164,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: new, stickerPacks: [new], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) return true } - case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite), let .participantJoinByRequest(invite, _): + case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite, _), let .participantJoinByRequest(invite, _): if let inviteLink = invite.link, !inviteLink.hasSuffix("...") { if invite.isPermanent { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 84eb66b1d2..9543e29125 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -1418,7 +1418,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) - case let .participantJoinedViaInvite(invite): + case let .participantJoinedViaInvite(invite, joinedViaFolderLink): var peers = SimpleDictionary() var author: Peer? if let peer = self.entry.peers[self.entry.event.peerId] { @@ -1429,7 +1429,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString + if joinedViaFolderLink { + rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") + } else { + rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") + } appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 9cd461f90b..e34778b835 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -593,7 +593,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } case let .startAttach(peerId, payload, choose): let presentError: (String) -> Void = { errorText in - present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index abc2b0868a..832e879356 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -6080,7 +6080,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self, let peer = strongSelf.data?.peer { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) + let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) controller.keepOnParentDismissal = true strongSelf.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUIPreferences/BUILD b/submodules/TelegramUIPreferences/BUILD index d3a5d7b5b1..5a106b25ca 100644 --- a/submodules/TelegramUIPreferences/BUILD +++ b/submodules/TelegramUIPreferences/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/Postbox:Postbox", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Display:Display", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift index 34690fd75f..e62f227ee2 100644 --- a/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/PresentationThemeSettings.swift @@ -3,7 +3,12 @@ import UIKit import Postbox import TelegramCore import SwiftSignalKit -import Display + +private extension UIColor { + convenience init(rgb: UInt32) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) + } +} public enum PresentationBuiltinThemeReference: Int32 { case dayClassic = 0 diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 30028e1a96..6ab2793063 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -12,7 +12,7 @@ public enum UndoOverlayContent { case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) case succeed(text: String) - case info(title: String?, text: String) + case info(title: String?, text: String, timeout: Double?) case emoji(name: String, text: String) case swipeToReply(title: String, text: String) case actionSucceeded(title: String, text: String, cancel: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index aaa4abbdaa..2fe7ddeddd 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -173,7 +173,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 5 displayUndo = false self.originalRemainingSeconds = 3 - case let .info(title, text): + case let .info(title, text, timeout): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -193,11 +193,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 10 displayUndo = false - self.originalRemainingSeconds = Double(max(5, min(8, text.count / 14))) + if let timeout { + self.originalRemainingSeconds = timeout + } else { + self.originalRemainingSeconds = Double(max(5, min(8, text.count / 14))) + } if text.contains("](") { isUserInteractionEnabled = true } + case let .actionSucceeded(title, text, cancel): self.avatarNode = nil self.iconNode = nil