diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 29f9b3d515..02916252d0 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1392,13 +1392,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } + + let context = strongSelf.context + let filterPeersAreMuted: Signal = strongSelf.context.engine.peers.currentChatListFilters() + |> take(1) + |> mapToSignal { filters -> Signal in + guard let filter = filters.first(where: { $0.id == id }) else { + return .single(false) + } + guard case let .filter(_, _, _, data) = filter else { + return .single(false) + } + return context.engine.data.get( + EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.NotificationSettings.init(id:))) + ) + |> map { list -> Bool in + for item in list { + switch item.muteState { + case .default, .unmuted: + return false + default: + break + } + } + return true + } + } + let _ = combineLatest( queue: Queue.mainQueue(), strongSelf.context.engine.peers.currentChatListFilters(), strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) - ) - ).start(next: { [weak self] filters, premiumLimits in + ), + filterPeersAreMuted + ).start(next: { [weak self] filters, premiumLimits, filterPeersAreMuted in guard let strongSelf = self else { return } @@ -1546,6 +1574,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController for filter in filters { if filter.id == filterId, case let .filter(_, title, _, data) = filter { + if data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty && !data.includePeers.peers.isEmpty { + items.append(.action(ContextMenuActionItem(text: filterPeersAreMuted ? "Unmute All" : "Mute All", textColor: .primary, badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: filterPeersAreMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c.dismiss(completion: { + guard let strongSelf = self else { + return + } + + let _ = strongSelf.context.engine.peers.updateMultiplePeerMuteSettings(peerIds: data.includePeers.peers, muted: !filterPeersAreMuted).start() + }) + }))) + } + 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: data.hasSharedLinks ? nil : ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) @@ -2701,13 +2743,53 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private func shareFolder(filterId: Int32, data: ChatListFilterData, title: String) { - openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in + let presentationData = self.presentationData + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.8, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let signal: Signal<[ExportedChatFolderLink]?, NoError> = self.context.engine.peers.getExportedChatFolderLinks(id: filterId) + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let _ = (signal + |> deliverOnMainQueue).start(next: { [weak self] links in + guard let self else { + return + } + + let previewScreen = ChatFolderLinkPreviewScreen( + context: self.context, + subject: .linkList(folderId: filterId, initialLinks: links ?? []), + contents: ChatFolderLinkContents( + localFilterId: filterId, title: title, + peers: [], + alreadyMemberPeerIds: Set(), + memberCounts: [:] + ), + completion: nil + ) + self.push(previewScreen) + }) + + /*openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in self?.push(c) }, presentController: { [weak self] c in self?.present(c, in: .window(.root)) }, completed: { }, linkUpdated: { _ in - }) + })*/ } public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index e05fb5de5b..8e1856cae5 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1521,22 +1521,10 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat |> deliverOnMainQueue).start(next: { filters in updated(filters) - if let currentPreset, waitForSync { - let _ = (context.engine.peers.updatedChatListFilters() - |> filter { filters -> Bool in - for filter in filters { - if filter.id == currentPreset.id { - if let data = filter.data { - if Set(data.includePeers.peers) == Set(includePeers.peers) { - return true - } - } - } - } - return true - } + if waitForSync { + let _ = (context.engine.peers.chatListFiltersAreSynced() + |> filter { $0 } |> take(1) - |> delay(1.0, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { _ in completed() }) diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index 2acde4d64a..f13b364e50 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -166,7 +166,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { let arguments = arguments as! FolderInviteLinkListControllerArguments switch self { case let .header(text): - return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListNewFolder", sectionId: self.section) + return InviteLinkHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animationName: "ChatListCloudFolderLink", sectionId: self.section) case let .mainLinkHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .mainLink(link, isGenerating): @@ -517,13 +517,22 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese } } else { var isGroup = true + let isPrivate = peer.addressName == nil if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } if isGroup { - text = "You don't have the admin rights to share invite links to this group chat." + if isPrivate { + text = "You don't have the admin rights to share invite links to this private group chat." + } else { + text = "You don't have the admin rights to share invite links to this group chat." + } } else { - text = "You don't have the admin rights to share invite links to this channel." + if isPrivate { + text = "You don't have the admin rights to share invite links to this private channel." + } else { + text = "You don't have the admin rights to share invite links to this channel." + } } } dismissTooltipsImpl?() diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 21969299e6..5ad2da5654 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -151,7 +151,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { if strongSelf.item == nil { - strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: item.animationName), width: 256, height: 256, playbackMode: .count(1), mode: .direct(cachePathPrefix: nil)) strongSelf.animationNode.visibility = true } strongSelf.item = item diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift index e74c76ffec..b0ce7f0abe 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift @@ -197,6 +197,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { badgeIconName: badgeIconName, badgeText: "\(item.count)", badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount), + badgeGraphPosition: CGFloat(item.count) / CGFloat(item.premiumCount), isPremiumDisabled: item.isPremiumDisabled )) let containerSize = CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index bad7273a0f..6eff33fb93 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -44,6 +44,7 @@ private class PremiumLimitAnimationComponent: Component { private let textColor: UIColor private let badgeText: String? private let badgePosition: CGFloat + private let badgeGraphPosition: CGFloat private let isPremiumDisabled: Bool init( @@ -53,6 +54,7 @@ private class PremiumLimitAnimationComponent: Component { textColor: UIColor, badgeText: String?, badgePosition: CGFloat, + badgeGraphPosition: CGFloat, isPremiumDisabled: Bool ) { self.iconName = iconName @@ -61,6 +63,7 @@ private class PremiumLimitAnimationComponent: Component { self.textColor = textColor self.badgeText = badgeText self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition self.isPremiumDisabled = isPremiumDisabled } @@ -83,6 +86,9 @@ private class PremiumLimitAnimationComponent: Component { if lhs.badgePosition != rhs.badgePosition { return false } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } if lhs.isPremiumDisabled != rhs.isPremiumDisabled { return false } @@ -245,7 +251,7 @@ private class PremiumLimitAnimationComponent: Component { let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight)) self.container.frame = containerFrame - let activityPosition: CGFloat = floor(containerFrame.width * component.badgePosition) + let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition) let activeWidth: CGFloat = containerFrame.width - activityPosition if !component.isPremiumDisabled { @@ -433,6 +439,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { let badgeIconName: String? let badgeText: String? let badgePosition: CGFloat + let badgeGraphPosition: CGFloat let isPremiumDisabled: Bool public init( @@ -447,6 +454,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { badgeIconName: String?, badgeText: String?, badgePosition: CGFloat, + badgeGraphPosition: CGFloat, isPremiumDisabled: Bool ) { self.inactiveColor = inactiveColor @@ -460,6 +468,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { self.badgeIconName = badgeIconName self.badgeText = badgeText self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition self.isPremiumDisabled = isPremiumDisabled } @@ -497,6 +506,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { if lhs.badgePosition != rhs.badgePosition { return false } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } if lhs.isPremiumDisabled != rhs.isPremiumDisabled { return false } @@ -524,6 +536,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { textColor: component.activeTitleColor, badgeText: component.badgeText, badgePosition: component.badgePosition, + badgeGraphPosition: component.badgeGraphPosition, isPremiumDisabled: component.isPremiumDisabled ), availableSize: CGSize(width: context.availableSize.width, height: height), @@ -591,7 +604,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { transition: context.transition ) - let activityPosition = floor(context.availableSize.width * component.badgePosition) + let activityPosition = floor(context.availableSize.width * component.badgeGraphPosition) var inactiveValueOpacity: CGFloat = 1.0 if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 { @@ -747,6 +760,7 @@ private final class LimitSheetContent: CombinedComponent { let defaultValue: String let premiumValue: String let badgePosition: CGFloat + let badgeGraphPosition: CGFloat switch subject { case .folders: let limit = state.limits.maxFoldersCount @@ -757,6 +771,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if !state.isPremium && badgePosition > 0.5 { string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string @@ -775,6 +790,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if isPremiumDisabled { badgeText = "\(limit)" @@ -794,6 +810,11 @@ private final class LimitSheetContent: CombinedComponent { string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string defaultValue = count > limit ? "\(limit)" : "" premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)" + if count >= premiumLimit { + badgeGraphPosition = max(0.1, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) + } badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit)) if isPremiumDisabled { @@ -809,6 +830,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if isPremiumDisabled { badgeText = "\(limit)" @@ -823,6 +845,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition if isPremiumDisabled { badgeText = "\(limit)" @@ -837,6 +860,7 @@ private final class LimitSheetContent: CombinedComponent { defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" badgePosition = component.count == 4 ? 1.0 : 0.5 + badgeGraphPosition = badgePosition titleText = strings.Premium_FileTooLarge if isPremiumDisabled { @@ -856,6 +880,7 @@ private final class LimitSheetContent: CombinedComponent { } else { badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit)) } + badgeGraphPosition = badgePosition buttonAnimationName = "premium_addone" if isPremiumDisabled { @@ -931,6 +956,7 @@ private final class LimitSheetContent: CombinedComponent { badgeIconName: iconName, badgeText: badgeText, badgePosition: badgePosition, + badgeGraphPosition: badgeGraphPosition, isPremiumDisabled: isPremiumDisabled ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index d9340ea5e4..eb13f43ba2 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -170,6 +170,7 @@ private final class LimitComponent: CombinedComponent { badgeIconName: "", badgeText: nil, badgePosition: 0.0, + badgeGraphPosition: 0.0, isPremiumDisabled: false ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 1cadfcf3c5..86330a046b 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -241,7 +241,7 @@ public final class QrCodeScreen: ViewController { case .chatFolder: //TODO:localize title = "Invite by QR Code" - text = "Everyone on Telegram can scan this code to join your folder." + text = "Everyone on Telegram can scan this code to add this folder and join the chats included in this invite link." default: title = "" text = "" diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index af33514b6a..70eb29f827 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -107,7 +107,7 @@ extension UserLimitsConfiguration { self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength) self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText) self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1) - self.maxSharedFolderInviteLinks = getValue("chatlists_invites_limit", orElse: isPremium ? 100 : 3) - self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2) + self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3) + self.maxSharedFolderJoin = getValue("chatlist_joined_limit", orElse: isPremium ? 100 : 2) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index c6acb96500..5995ab2615 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -1319,6 +1319,14 @@ func requestChatListFiltersSync(transaction: Transaction) { transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeChatListFiltersOperation(content: .sync)) } +func _internal_chatListFiltersAreSynced(postbox: Postbox) -> Signal { + return postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizeChatListFilters, limit: 1) + |> map { view -> Bool in + return view.entries.isEmpty + } + |> distinctUntilChanged +} + func managedChatListFilters(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { return Signal { _ in let updateFeaturedDisposable = _internal_updateChatListFeaturedFilters(postbox: postbox, network: network).start() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 8550402677..2ece79bff4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -245,6 +245,15 @@ public extension TelegramEngine { public func updatePeerMuteSetting(peerId: PeerId, threadId: Int64?, muteInterval: Int32?) -> Signal { return _internal_updatePeerMuteSetting(account: self.account, peerId: peerId, threadId: threadId, muteInterval: muteInterval) } + + public func updateMultiplePeerMuteSettings(peerIds: [EnginePeer.Id], muted: Bool) -> Signal { + return self.account.postbox.transaction { transaction -> Void in + for peerId in peerIds { + _internal_updatePeerMuteSetting(account: self.account, transaction: transaction, peerId: peerId, threadId: nil, muteInterval: muted ? Int32.max : nil) + } + } + |> ignoreValues + } public func updatePeerDisplayPreviewsSetting(peerId: PeerId, threadId: Int64?, displayPreviews: PeerNotificationDisplayPreviews) -> Signal { return _internal_updatePeerDisplayPreviewsSetting(account: self.account, peerId: peerId, threadId: threadId, displayPreviews: displayPreviews) @@ -512,6 +521,10 @@ public extension TelegramEngine { public func updatedChatListFilters() -> Signal<[ChatListFilter], NoError> { return _internal_updatedChatListFilters(postbox: self.account.postbox, hiddenIds: self.account.viewTracker.hiddenChatListFilterIds) } + + public func chatListFiltersAreSynced() -> Signal { + return _internal_chatListFiltersAreSynced(postbox: self.account.postbox) + } public func updatedChatListFiltersInfo() -> Signal<(filters: [ChatListFilter], synchronized: Bool), NoError> { return _internal_updatedChatListFiltersInfo(postbox: self.account.postbox) diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 1572124149..585ce7411f 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/Markdown", "//submodules/UndoUI", "//submodules/PremiumUI", + "//submodules/QrCodeUI", + "//submodules/InviteLinksUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift new file mode 100644 index 0000000000..9e4fa31343 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ActionListItemComponent.swift @@ -0,0 +1,196 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData + +final class ActionListItemComponent: Component { + let theme: PresentationTheme + let sideInset: CGFloat + let iconName: String? + let title: String + let hasNext: Bool + let action: () -> Void + + init( + theme: PresentationTheme, + sideInset: CGFloat, + iconName: String?, + title: String, + hasNext: Bool, + action: @escaping () -> Void + ) { + self.theme = theme + self.sideInset = sideInset + self.iconName = iconName + self.title = title + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let iconView: UIImageView + private let separatorLayer: SimpleLayer + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: ActionListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + self.iconView = UIImageView() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + + self.containerButton.addSubview(self.iconView) + + self.containerButton.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + if self.component?.iconName != component.iconName { + if let iconName = component.iconName { + self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate) + } else { + self.iconView.image = nil + } + } + if themeUpdated { + self.iconView.tintColor = component.theme.list.itemAccentColor + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 44.0 + let verticalInset: CGFloat = 1.0 + let leftInset: CGFloat = 62.0 + component.sideInset + let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + + let previousTitleFrame = self.title.view?.frame + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let centralContentHeight: CGFloat = titleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + } + + if let iconImage = self.iconView.image { + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift index 136936f094..10536d30d5 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/ChatFolderLinkPreviewScreen.swift @@ -17,6 +17,9 @@ import Markdown import UndoUI import PremiumUI import ButtonComponent +import ContextUI +import QrCodeUI +import InviteLinksUI private final class ChatFolderLinkPreviewScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -102,6 +105,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { private var selectedItems = Set() + private var linkListItems: [ExportedChatFolderLink] = [] + private let bottomOverscrollLimit: CGFloat private var ignoreScrolling: Bool = false @@ -324,6 +329,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } } + if self.component == nil, case let .linkList(_, initialLinks) = component.subject { + self.linkListItems = initialLinks + } + self.component = component self.state = state self.environment = environment @@ -364,7 +373,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let titleString: String var allChatsAdded = false - if let linkContents = component.linkContents { + if case .linkList = component.subject { + //TODO:localize + titleString = "Share Folder" + } else if let linkContents = component.linkContents { //TODO:localize if case .remove = component.subject { titleString = "Remove Folder" @@ -406,7 +418,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentHeight += 14.0 var topBadge: String? - if case .remove = component.subject { + if case .linkList = component.subject { + } else if case .remove = component.subject { } else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil { topBadge = "+\(linkContents.peers.count)" } @@ -435,7 +448,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { contentHeight += 20.0 let text: String - if let linkContents = component.linkContents { + if case .linkList = component.subject { + text = "Create more links to set up different access\nlevels for different people." + } else if let linkContents = component.linkContents { if case .remove = component.subject { text = "Do you also want to quit the chats included in this folder?" } else if allChatsAdded { @@ -493,90 +508,257 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { var itemsHeight: CGFloat = 0.0 var validIds: [AnyHashable] = [] - if let linkContents = component.linkContents { + if case let .linkList(folderId, _) = component.subject { + do { + let id = AnyHashable("action") + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item + } + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(ActionListItemComponent( + theme: environment.theme, + sideInset: 0.0, + iconName: "Contact List/LinkActionIcon", + title: "Create a New Link", + hasNext: !self.linkListItems.isEmpty, + action: { [weak self] in + self?.openCreateLink() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height + } + + for i in 0 ..< self.linkListItems.count { + let link = self.linkListItems[i] + + let id = AnyHashable(link.link) + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item + } + + let subtitle: String + if link.peerIds.count == 1 { + subtitle = "includes 1 chat" + } else { + subtitle = "includes \(link.peerIds.count) chats" + } + + let itemComponent = LinkListItemComponent( + theme: environment.theme, + sideInset: 0.0, + title: link.title.isEmpty ? link.link : link.title, + link: link, + label: subtitle, + selectionState: .none, + hasNext: i != self.linkListItems.count - 1, + action: { [weak self] link in + guard let self else { + return + } + self.openLink(link: link) + }, + contextAction: { [weak self] link, sourceView, gesture in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = link.link + + if let self, let component = self.component, let controller = self.environment?.controller() { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + }))) + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component, let controller = self.environment?.controller() { + controller.present(QrCodeScreen(context: component.context, updatedPresentationData: nil, subject: .chatFolder(slug: link.slug)), in: .window(.root)) + } + }))) + + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component { + self.linkListItems.removeAll(where: { $0.link == link.link }) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + + let context = component.context + let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true) + |> deliverOnMainQueue).start(completed: { + let _ = (context.engine.peers.deleteChatFolderLink(filterId: folderId, link: link) + |> deliverOnMainQueue).start(completed: { + }) + }) + } + }))) + + let items = ContextController.Items(content: .list(itemList)) + + let controller = ContextController( + account: component.context.account, + presentationData: presentationData, + source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)), + items: .single(items), + recognizer: nil, + gesture: gesture + ) + + environment.controller()?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + environment.controller()?.presentInGlobalOverlay(controller) + } + ) + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(itemComponent), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height + } + } else if let linkContents = component.linkContents { for i in 0 ..< linkContents.peers.count { let peer = linkContents.peers[i] - for _ in 0 ..< 1 { - //let id: AnyHashable = AnyHashable("\(peer.id)_\(j)") - let id = AnyHashable(peer.id) - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - 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( - context: component.context, - theme: environment.theme, - strings: environment.strings, - sideInset: 0.0, - title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer, - 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 - guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { - return - } - - if case .remove = component.subject { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = "You are already a member of this channel." - } else { - text = "You are already a member of this group." - } - controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) - } else { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height + let id = AnyHashable(peer.id) + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + 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( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: 0.0, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + 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 + guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { + return + } + + if case .remove = component.subject { + if self.selectedItems.contains(peer.id) { + self.selectedItems.remove(peer.id) + } else { + self.selectedItems.insert(peer.id) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let text: String + if case let .channel(channel) = peer, case .broadcast = channel.info { + text = "You are already a member of this channel." + } else { + text = "You are already a member of this group." + } + controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) + } else { + if self.selectedItems.contains(peer.id) { + self.selectedItems.remove(peer.id) + } else { + self.selectedItems.insert(peer.id) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height } } @@ -592,7 +774,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } let listHeaderTitle: String - if let linkContents = component.linkContents { + if case .linkList = component.subject { + listHeaderTitle = "INVITE LINKS" + } else if let linkContents = component.linkContents { if case .remove = component.subject { if linkContents.peers.count == 1 { listHeaderTitle = "1 CHAT TO QUIT" @@ -839,21 +1023,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { } controller.dismiss() - - /*self.joinDisposable = (component.context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(self.selectedItems)) - |> deliverOnMainQueue).start(completed: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - })*/ } else if allChatsAdded { controller.dismiss() } else if let _ = component.linkContents { if self.joinDisposable == nil, !self.selectedItems.isEmpty { let joinSignal: Signal switch component.subject { - case .remove: + case .linkList, .remove: return case let .slug(slug): joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems)) @@ -961,40 +1137,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) - /*let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(SolidRoundedButtonComponent( - title: actionButtonTitle, - badge: (self.selectedItems.isEmpty || allChatsAdded) ? nil : "\(self.selectedItems.count)", - theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 11.0, - gloss: false, - isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, - animationName: nil, - iconPosition: .right, - iconSpacing: 4.0, - isLoading: self.inProgress, - action: { [weak self] in - - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) - )*/ - let bottomPanelHeight = 14.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) - } - 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))) + var bottomPanelHeight: CGFloat = 0.0 + + if case .linkList = component.subject { + bottomPanelHeight += 30.0 + } else { + bottomPanelHeight += 14.0 + environment.safeInsets.bottom + actionButtonSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + 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( @@ -1015,9 +1175,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { let containerInset: CGFloat = environment.statusBarHeight + 10.0 let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) - let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset) - - //self.scrollContentClippingView.layer.cornerRadius = 10.0 + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight) @@ -1026,12 +1184,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { 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 - 8.0 - (containerInset + 56.0))) + let scrollClippingFrame: CGRect + if case .linkList = component.subject { + scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - (containerInset + 56.0) + 1000.0)) + } else { + scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - bottomPanelHeight - 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)) self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset))) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize @@ -1044,6 +1207,168 @@ private final class ChatFolderLinkPreviewScreenComponent: Component { return availableSize } + + private func openLink(link: ExportedChatFolderLink) { + guard let component = self.component else { + return + } + guard case let .linkList(folderId, _) = component.subject else { + return + } + + let _ = (component.context.engine.peers.currentChatListFilters() + |> deliverOnMainQueue).start(next: { [weak self] filters in + guard let self, let component = self.component else { + return + } + guard let filter = filters.first(where: { $0.id == folderId }) else { + return + } + guard case let .filter(_, title, _, data) = filter else { + return + } + + let peerIds = data.includePeers.peers + let _ = (component.context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + let peers = peers.compactMap({ peer -> EnginePeer? in + guard let peer else { + return nil + } + if case let .legacyGroup(group) = peer, group.migrationReference != nil { + return nil + } + return peer + }) + + let navigationController = controller.navigationController + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in + (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + })) + controller.dismiss() + }) + }) + } + + private func openCreateLink() { + guard let component = self.component else { + return + } + guard case let .linkList(folderId, _) = component.subject else { + return + } + + let _ = (component.context.engine.peers.currentChatListFilters() + |> deliverOnMainQueue).start(next: { [weak self] filters in + guard let self, let component = self.component else { + return + } + guard let filter = filters.first(where: { $0.id == folderId }) else { + return + } + guard case let .filter(_, title, _, data) = filter else { + return + } + + let peerIds = data.includePeers.peers + let _ = (component.context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] peers in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + let peers = peers.compactMap({ peer -> EnginePeer? in + guard let peer else { + return nil + } + if case let .legacyGroup(group) = peer, group.migrationReference != nil { + return nil + } + return peer + }) + if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { + let navigationController = controller.navigationController + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: { _ in }, presentController: { [weak navigationController] c in + (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + })) + } else { + var enabledPeerIds: [EnginePeer.Id] = [] + for peer in peers { + if canShareLinkToPeer(peer: peer) { + enabledPeerIds.append(peer.id) + } + } + + let _ = (component.context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: enabledPeerIds) + |> deliverOnMainQueue).start(next: { [weak self] link in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + self.linkListItems.insert(link, at: 0) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + + let navigationController = controller.navigationController + controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { [weak self] updatedLink in + guard let self else { + return + } + if let index = self.linkListItems.firstIndex(where: { $0.link == link.link }) { + if let updatedLink { + self.linkListItems[index] = updatedLink + } else { + self.linkListItems.remove(at: index) + } + } else { + if let updatedLink { + self.linkListItems.insert(updatedLink, at: 0) + } + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + }, presentController: { [weak navigationController] c in + (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) + })) + + controller.dismiss() + }, error: { [weak self] error in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + //TODO:localize + let text: String + switch error { + case .generic: + text = "An error occurred" + case let .sharedFolderLimitExceeded(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { + }) + + controller.push(limitController) + + return + case let .limitExceeded(limit, _): + let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: { + }) + controller.push(limitController) + + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + } + }) + }) + } } func makeView() -> View { @@ -1060,6 +1385,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { case slug(String) case updates(ChatFolderUpdates) case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id]) + case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink]) } private let context: AccountContext @@ -1116,3 +1442,25 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer { } } } + +private final class LinkListContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift new file mode 100644 index 0000000000..ae5ece266c --- /dev/null +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/LinkListItemComponent.swift @@ -0,0 +1,330 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import TelegramCore +import CheckNode + +func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } +} + +final class LinkListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + let theme: PresentationTheme + let sideInset: CGFloat + let title: String + let link: ExportedChatFolderLink + let label: String + let selectionState: SelectionState + let hasNext: Bool + let action: (ExportedChatFolderLink) -> Void + let contextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void + + init( + theme: PresentationTheme, + sideInset: CGFloat, + title: String, + link: ExportedChatFolderLink, + label: String, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (ExportedChatFolderLink) -> Void, + contextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void + ) { + self.theme = theme + self.sideInset = sideInset + self.title = title + self.link = link + self.label = label + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + self.contextAction = contextAction + } + + static func ==(lhs: LinkListItemComponent, rhs: LinkListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.link != rhs.link { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: ContextControllerSourceView { + private let extractedContainerView: ContextExtractedContentContainingView + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let iconView: UIImageView + private let iconBackgroundView: UIImageView + + private var checkLayer: CheckLayer? + + private var isExtractedToContextMenu: Bool = false + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: LinkListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.extractedContainerView = ContextExtractedContentContainingView() + self.containerButton = HighlightTrackingButton() + + self.iconView = UIImageView() + self.iconBackgroundView = UIImageView() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.addSubview(self.containerButton) + + self.containerButton.addSubview(self.iconBackgroundView) + self.containerButton.addSubview(self.iconView) + + self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + self.containerButton.clipsToBounds = value + self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil + self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 + } + self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in + guard let self else { + return + } + self.isExtractedToContextMenu = value + + let mappedTransition: Transition + if value { + mappedTransition = Transition(transition) + } else { + mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + self.state?.updated(transition: mappedTransition) + } + + self.containerButton.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted, case .none = component.selectionState { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction(component.link, self.extractedContainerView, gesture) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(component.link) + } + + func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0 + + let height: CGFloat = 60.0 + let verticalInset: CGFloat = 1.0 + var leftInset: CGFloat = 62.0 + component.sideInset + var iconLeftInset: CGFloat = component.sideInset + + if case let .editing(isSelected) = component.selectionState { + leftInset += 48.0 + iconLeftInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset + + if themeUpdated { + self.iconBackgroundView.image = generateFilledCircleImage(diameter: 40.0, color: component.theme.list.itemCheckColors.fillColor) + self.iconView.image = UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = component.theme.list.itemCheckColors.foregroundColor + } + + if let iconImage = self.iconView.image { + transition.setFrame(view: self.iconBackgroundView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - 40.0) * 0.5), y: floor((height - 40.0) * 0.5)), size: CGSize(width: 40.0, height: 40.0))) + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconImage.size.width) * 0.5), y: floor((height - iconImage.size.height) * 0.5)), size: iconImage.size)) + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.label, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSpacing: CGFloat = 1.0 + + let contentHeight: CGFloat = titleSize.height + titleSpacing + labelSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - contentHeight) / 2.0)), size: titleSize) + let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + titleView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + labelView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(labelView) + } + transition.setPosition(view: labelView, position: labelFrame.origin) + labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.extractedContainerView, frame: resultBounds) + transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds) + self.extractedContainerView.contentRect = resultBounds + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift index 1ee2e163ef..4f8e5a3ad9 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/Sources/PeerListItemComponent.swift @@ -15,19 +15,6 @@ import TelegramStringFormatting private let avatarFont = avatarPlaceholderFont(size: 15.0) -private func cancelContextGestures(view: UIView) { - if let gestureRecognizers = view.gestureRecognizers { - for gesture in gestureRecognizers { - if let gesture = gesture as? ContextGesture { - gesture.cancel() - } - } - } - for subview in view.subviews { - cancelContextGestures(view: subview) - } -} - final class PeerListItemComponent: Component { enum SelectionState: Equatable { case none diff --git a/submodules/TelegramUI/Resources/Animations/ChatListCloudFolderLink.tgs b/submodules/TelegramUI/Resources/Animations/ChatListCloudFolderLink.tgs new file mode 100644 index 0000000000..3e71d25b40 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/ChatListCloudFolderLink.tgs differ