From 796826b0addc6ada4d676610f22df37efddebe87 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 31 Mar 2026 22:42:02 +0800 Subject: [PATCH] Various improvements --- .../Sources/PeerSelectionController.swift | 5 +- .../Sources/Node/ChatListNode.swift | 731 ++++++++++-------- .../Sources/Node/ChatListNodeEntries.swift | 36 +- .../Sources/PeerSelectionController.swift | 4 +- .../Sources/PeerSelectionControllerNode.swift | 6 +- .../Components/StorageUsageScreen/BUILD | 2 + .../StorageFileListPanelComponent.swift | 102 ++- .../StoragePeerListPanelComponent.swift | 95 ++- .../StorageUsagePanelContainerComponent.swift | 416 +++------- .../Sources/StorageUsageScreen.swift | 14 +- .../Chat/ChatMessageActionOptions.swift | 157 ++-- .../ContactMultiselectionControllerNode.swift | 2 +- 12 files changed, 766 insertions(+), 804 deletions(-) diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 2a63824a1f..547d98a0e6 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -67,6 +67,7 @@ public final class PeerSelectionControllerParams { public let hasCreation: Bool public let immediatelySwitchToContacts: Bool public let immediatelyActivateMultipleSelection: Bool + public let suggestedPeers: [EnginePeer] public init( context: AccountContext, @@ -89,7 +90,8 @@ public final class PeerSelectionControllerParams { selectForumThreads: Bool = false, hasCreation: Bool = false, immediatelySwitchToContacts: Bool = false, - immediatelyActivateMultipleSelection: Bool = false + immediatelyActivateMultipleSelection: Bool = false, + suggestedPeers: [EnginePeer] = [] ) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -112,6 +114,7 @@ public final class PeerSelectionControllerParams { self.hasCreation = hasCreation self.immediatelySwitchToContacts = immediatelySwitchToContacts self.immediatelyActivateMultipleSelection = immediatelyActivateMultipleSelection + self.suggestedPeers = suggestedPeers } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 4236fb048a..75a63892a7 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -27,7 +27,7 @@ import GlobalControlPanelsContext public enum ChatListNodeMode { case chatList(appendContacts: Bool) - case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) + case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], topPeers: [EnginePeer], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) case peerType(type: [ReplyMarkupButtonRequestPeerType], hasCreate: Bool) } @@ -372,286 +372,268 @@ public struct ChatListNodeState: Equatable { } } -private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { +private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry], presentationData: ChatListPresentationData) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case .HeaderEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) - case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): - var header: ChatListSearchItemHeader? - if case .peerType = mode { - } else { - if case .action = appearance { - // TODO: hack, generalize - header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) - } + case .HeaderEntry: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) + case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): + var header: ChatListSearchItemHeader? + if case .peerType = mode { + } else { + if case .action = appearance { + // TODO: hack, generalize + header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( - presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), - context: context, - title: title, - image: image, - appearance: appearance, - isSelected: selected, - header: header, - action: { - nodeInteraction.additionalCategorySelected(id) - } - ), directionHint: entry.directionHint) - case let .PeerEntry(peerEntry): - let index = peerEntry.index - let presentationData = peerEntry.presentationData - let combinedReadState = peerEntry.readState - let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount - let draftState = peerEntry.draftState - let peer = peerEntry.peer - let threadInfo = peerEntry.threadInfo - let presence = peerEntry.presence - let hasUnseenMentions = peerEntry.hasUnseenMentions - let hasUnseenReactions = peerEntry.hasUnseenReactions - let hasUnseenPollVotes = peerEntry.hasUnseenPollVotes - let editing = peerEntry.editing - let hasActiveRevealControls = peerEntry.hasActiveRevealControls - let selected = peerEntry.selected - let inputActivities = peerEntry.inputActivities - let promoInfo = peerEntry.promoInfo - let hasFailedMessages = peerEntry.hasFailedMessages - let isContact = peerEntry.isContact - let forumTopicData = peerEntry.forumTopicData - let topForumTopicItems = peerEntry.topForumTopicItems - let revealed = peerEntry.revealed - - switch mode { - case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( - presentationData: presentationData, - context: context, - chatListLocation: location, - filterData: filterData, - index: index, - content: .peer(ChatListItemContent.PeerData( - messages: peerEntry.messages, - peer: peer, - threadInfo: threadInfo, - combinedReadState: combinedReadState, - isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, - presence: presence, - hasUnseenMentions: hasUnseenMentions, - hasUnseenReactions: hasUnseenReactions, - hasUnseenPollVotes: hasUnseenPollVotes, - draftState: draftState, - mediaDraftContentType: peerEntry.mediaDraftContentType, - inputActivities: inputActivities, - promoInfo: promoInfo, - ignoreUnreadBadge: false, - displayAsMessage: false, - hasFailedMessages: hasFailedMessages, - forumTopicData: forumTopicData, - topForumTopicItems: topForumTopicItems, - autoremoveTimeout: peerEntry.autoremoveTimeout, - storyState: peerEntry.storyState.flatMap { storyState in - return ChatListItemContent.StoryState( - stats: storyState.stats, - hasUnseenCloseFriends: storyState.hasUnseenCloseFriends - ) - }, - requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - displayAsTopicList: peerEntry.displayAsTopicList, - tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) - )), - editing: editing, - hasActiveRevealControls: hasActiveRevealControls, - selected: selected, - header: nil, - enabledContextActions: .auto, - hiddenOffset: threadInfo?.isHidden == true && !revealed, - interaction: nodeInteraction - ), directionHint: entry.directionHint) - case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): - let itemPeer = peer.chatOrMonoforumMainPeer - var chatPeer: EnginePeer? - if let peer = peer.peers[peer.peerId] { - chatPeer = peer - } - var enabled = true - if let isPeerEnabled { - if let itemPeer { - enabled = isPeerEnabled(itemPeer) - } - } else { - if filter.contains(.onlyWriteable) { - if let peer = peer.peers[peer.peerId] { - if !canSendMessagesToPeer(peer._asPeer()) { - enabled = false - } - if peerEntry.requiresPremiumForMessaging { - enabled = false - } - } else { - enabled = false - } - - if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer { - if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) { - } else { - enabled = false - } - } - } - if filter.contains(.onlyPrivateChats) { - if let peer = peer.peers[peer.peerId] { - switch peer { - case .user, .secretChat: - break - default: - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyGroups) { - if let peer = peer.peers[peer.peerId] { - if case .legacyGroup = peer { - } else if case let .channel(peer) = peer, case .group = peer.info { - } else { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyManageable) { - if let peer = peer.peers[peer.peerId] { - var canManage = false - if case let .legacyGroup(peer) = peer { - switch peer.role { - case .creator, .admin: - canManage = true - default: - break - } - } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { - canManage = true - } else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) { - canManage = true - } - - if canManage { - } else { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.excludeChannels) { - if let peer = peer.peers[peer.peerId] { - if case let .channel(peer) = peer, case .broadcast = peer.info { - enabled = false - } - } - } - } - - var header: ChatListSearchItemHeader? - switch mode { - case let .peers(_, _, additionalCategories, _, _, _): - if !additionalCategories.isEmpty { - let headerType: ChatListSearchItemHeaderType - if case .action = additionalCategories[0].appearance { - // TODO: hack, generalize - headerType = .orImportIntoAnExistingGroup - } else { - headerType = .chats - } - header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) - } - default: - break - } - - var status: ContactsPeerItemStatus = .none - if isSelecting, let itemPeer = itemPeer { - if displayPresence, let presence = presence { - status = .presence(presence, presentationData.dateTimeFormat) - } else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) { - status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon) - } else { - status = .none - } - } - - let peerContent: ContactsPeerItemPeer - if let threadInfo = threadInfo, let itemPeer = itemPeer { - peerContent = .thread(peer: itemPeer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor) - } else { - peerContent = .peer(peer: itemPeer, chatPeer: chatPeer) - } - - var threadId: Int64? - switch index { - case let .forum(_, _, threadIdValue, _, _): - threadId = threadIdValue - case .chatList: - break - } - - var isForum = false - if let peer = chatPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum { - isForum = true - if editing, case .chatList = mode { - enabled = false - } - } - - var selectable = editing - if case .chatList = mode { - if isForum { - selectable = false - } - } - - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( - presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), - sortOrder: presentationData.nameSortOrder, - displayOrder: presentationData.nameDisplayOrder, - context: context, - peerMode: .generalSearch(isSavedMessages: false), - peer: peerContent, - status: status, - requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - enabled: enabled, - selection: selectable ? .selectable(selected: selected) : .none, - editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), - index: nil, - header: header, - action: { _ in - if let chatPeer = chatPeer { - if editing { - nodeInteraction.togglePeerSelected(chatPeer, threadId) - } else { - nodeInteraction.peerSelected(chatPeer, nil, threadId, nil, false) - } - } - }, disabledAction: (isForum && editing) && !peerEntry.requiresPremiumForMessaging ? nil : { _ in - if let chatPeer = chatPeer { - nodeInteraction.disabledPeerSelected(chatPeer, threadId, peerEntry.requiresPremiumForMessaging ? .premiumRequired : .generic) - } + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + context: context, + title: title, + image: image, + appearance: appearance, + isSelected: selected, + header: header, + action: { + nodeInteraction.additionalCategorySelected(id) + } + ), directionHint: entry.directionHint) + case let .TopPeer(_, peer): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch(isSavedMessages: false), + peer: .peer(peer: peer, chatPeer: peer), + status: .none, + requiresPremiumForMessaging: false, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: nil, + action: { _ in + nodeInteraction.peerSelected(peer, nil, nil, nil, false) + }, + disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) + case let .PeerEntry(peerEntry): + let index = peerEntry.index + let presentationData = peerEntry.presentationData + let combinedReadState = peerEntry.readState + let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount + let draftState = peerEntry.draftState + let peer = peerEntry.peer + let threadInfo = peerEntry.threadInfo + let presence = peerEntry.presence + let hasUnseenMentions = peerEntry.hasUnseenMentions + let hasUnseenReactions = peerEntry.hasUnseenReactions + let hasUnseenPollVotes = peerEntry.hasUnseenPollVotes + let editing = peerEntry.editing + let hasActiveRevealControls = peerEntry.hasActiveRevealControls + let selected = peerEntry.selected + let inputActivities = peerEntry.inputActivities + let promoInfo = peerEntry.promoInfo + let hasFailedMessages = peerEntry.hasFailedMessages + let isContact = peerEntry.isContact + let forumTopicData = peerEntry.forumTopicData + let topForumTopicItems = peerEntry.topForumTopicItems + let revealed = peerEntry.revealed + + switch mode { + case .chatList: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( + presentationData: presentationData, + context: context, + chatListLocation: location, + filterData: filterData, + index: index, + content: .peer(ChatListItemContent.PeerData( + messages: peerEntry.messages, + peer: peer, + threadInfo: threadInfo, + combinedReadState: combinedReadState, + isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, + presence: presence, + hasUnseenMentions: hasUnseenMentions, + hasUnseenReactions: hasUnseenReactions, + hasUnseenPollVotes: hasUnseenPollVotes, + draftState: draftState, + mediaDraftContentType: peerEntry.mediaDraftContentType, + inputActivities: inputActivities, + promoInfo: promoInfo, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: hasFailedMessages, + forumTopicData: forumTopicData, + topForumTopicItems: topForumTopicItems, + autoremoveTimeout: peerEntry.autoremoveTimeout, + storyState: peerEntry.storyState.flatMap { storyState in + return ChatListItemContent.StoryState( + stats: storyState.stats, + hasUnseenCloseFriends: storyState.hasUnseenCloseFriends + ) }, - animationCache: nodeInteraction.animationCache, - animationRenderer: nodeInteraction.animationRenderer - ), directionHint: entry.directionHint) - case .peerType: + requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, + displayAsTopicList: peerEntry.displayAsTopicList, + tags: chatListItemTags(location: location, accountPeerId: context.account.peerId, isPremium: isPremium, peer: peer.chatMainPeer, isUnread: combinedReadState?.isUnread ?? false, isMuted: isRemovedFromTotalUnreadCount, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: chatListFilters) + )), + editing: editing, + hasActiveRevealControls: hasActiveRevealControls, + selected: selected, + header: nil, + enabledContextActions: .auto, + hiddenOffset: threadInfo?.isHidden == true && !revealed, + interaction: nodeInteraction + ), directionHint: entry.directionHint) + case let .peers(filter, isSelecting, _, _, filters, displayAutoremoveTimeout, displayPresence): let itemPeer = peer.chatOrMonoforumMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { chatPeer = peer } + var enabled = true + if let isPeerEnabled { + if let itemPeer { + enabled = isPeerEnabled(itemPeer) + } + } else { + if filter.contains(.onlyWriteable) { + if let peer = peer.peers[peer.peerId] { + if !canSendMessagesToPeer(peer._asPeer()) { + enabled = false + } + if peerEntry.requiresPremiumForMessaging { + enabled = false + } + } else { + enabled = false + } + + if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer { + if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) { + } else { + enabled = false + } + } + } + if filter.contains(.onlyPrivateChats) { + if let peer = peer.peers[peer.peerId] { + switch peer { + case .user, .secretChat: + break + default: + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyGroups) { + if let peer = peer.peers[peer.peerId] { + if case .legacyGroup = peer { + } else if case let .channel(peer) = peer, case .group = peer.info { + } else { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.onlyManageable) { + if let peer = peer.peers[peer.peerId] { + var canManage = false + if case let .legacyGroup(peer) = peer { + switch peer.role { + case .creator, .admin: + canManage = true + default: + break + } + } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { + canManage = true + } else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) { + canManage = true + } + + if canManage { + } else { + enabled = false + } + } else { + enabled = false + } + } + if filter.contains(.excludeChannels) { + if let peer = peer.peers[peer.peerId] { + if case let .channel(peer) = peer, case .broadcast = peer.info { + enabled = false + } + } + } + } - let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) - let status: ContactsPeerItemStatus = .none + var header: ChatListSearchItemHeader? + switch mode { + case let .peers(_, _, additionalCategories, _, _, _, _): + if !additionalCategories.isEmpty { + let headerType: ChatListSearchItemHeaderType + if case .action = additionalCategories[0].appearance { + // TODO: hack, generalize + headerType = .orImportIntoAnExistingGroup + } else { + headerType = .chats + } + header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } + default: + break + } + var status: ContactsPeerItemStatus = .none + if isSelecting, let itemPeer = itemPeer { + if displayPresence, let presence = presence { + status = .presence(presence, presentationData.dateTimeFormat) + } else if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) { + status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon) + } else { + status = .none + } + } + + let peerContent: ContactsPeerItemPeer + if let threadInfo = threadInfo, let itemPeer = itemPeer { + peerContent = .thread(peer: itemPeer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor) + } else { + peerContent = .peer(peer: itemPeer, chatPeer: chatPeer) + } + + var threadId: Int64? + switch index { + case let .forum(_, _, threadIdValue, _, _): + threadId = threadIdValue + case .chatList: + break + } + + var isForum = false + if let peer = chatPeer, case let .channel(channel) = peer, channel.isForumOrMonoForum { + isForum = true + if editing, case .chatList = mode { + enabled = false + } + } + + var selectable = editing + if case .chatList = mode { + if isForum { + selectable = false + } + } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: presentationData.nameSortOrder, @@ -661,69 +643,37 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL peer: peerContent, status: status, requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, - enabled: true, - selection: editing ? .selectable(selected: selected) : .none, + enabled: enabled, + selection: selectable ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, - header: nil, + header: header, action: { _ in if let chatPeer = chatPeer { if editing { - nodeInteraction.togglePeerSelected(chatPeer, nil) + nodeInteraction.togglePeerSelected(chatPeer, threadId) } else { - nodeInteraction.peerSelected(chatPeer, nil, nil, nil, false) + nodeInteraction.peerSelected(chatPeer, nil, threadId, nil, false) } } - }, disabledAction: peerEntry.requiresPremiumForMessaging ? { _ in - if let chatPeer { - nodeInteraction.disabledPeerSelected(chatPeer, nil, .premiumRequired) + }, disabledAction: (isForum && editing) && !peerEntry.requiresPremiumForMessaging ? nil : { _ in + if let chatPeer = chatPeer { + nodeInteraction.disabledPeerSelected(chatPeer, threadId, peerEntry.requiresPremiumForMessaging ? .premiumRequired : .generic) } - } : nil, + }, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) + case .peerType: + let itemPeer = peer.chatOrMonoforumMainPeer + var chatPeer: EnginePeer? + if let peer = peer.peers[peer.peerId] { + chatPeer = peer } - case let .HoleEntry(_, theme): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) - case let .GroupReferenceEntry(groupReferenceEntry): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( - presentationData: groupReferenceEntry.presentationData, - context: context, - chatListLocation: location, - filterData: filterData, - index: groupReferenceEntry.index, - content: .groupReference(ChatListItemContent.GroupReferenceData( - groupId: groupReferenceEntry.groupId, - peers: groupReferenceEntry.peers, - message: groupReferenceEntry.message, - unreadCount: groupReferenceEntry.unreadCount, - hiddenByDefault: groupReferenceEntry.hiddenByDefault, - appearsPinned: groupReferenceEntry.appearsPinned, - storyState: groupReferenceEntry.storyState.flatMap { storyState in - return ChatListItemContent.StoryState( - stats: storyState.stats, - hasUnseenCloseFriends: storyState.hasUnseenCloseFriends - ) - } - )), - editing: groupReferenceEntry.editing, - hasActiveRevealControls: false, - selected: false, - header: nil, - enabledContextActions: .auto, - hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, - interaction: nodeInteraction - ), directionHint: entry.directionHint) - case let .ContactEntry(contactEntry): - let header: ChatListSearchItemHeader? = nil - var status: ContactsPeerItemStatus = .none - status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat) - - let presentationData = contactEntry.presentationData - - let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer) - + let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) + let status: ContactsPeerItemStatus = .none + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: presentationData.nameSortOrder, @@ -732,31 +682,103 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL peerMode: .generalSearch(isSavedMessages: false), peer: peerContent, status: status, + requiresPremiumForMessaging: peerEntry.requiresPremiumForMessaging, enabled: true, - selection: .none, + selection: editing ? .selectable(selected: selected) : .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, - header: header, + header: nil, action: { _ in - nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil, false) - }, - disabledAction: nil, + if let chatPeer = chatPeer { + if editing { + nodeInteraction.togglePeerSelected(chatPeer, nil) + } else { + nodeInteraction.peerSelected(chatPeer, nil, nil, nil, false) + } + } + }, disabledAction: peerEntry.requiresPremiumForMessaging ? { _ in + if let chatPeer { + nodeInteraction.disabledPeerSelected(chatPeer, nil, .premiumRequired) + } + } : nil, animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) - case let .ArchiveIntro(presentationData): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) - case let .EmptyIntro(presentationData): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) - case let .SectionHeader(presentationData, displayHide): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? { - hideChatListContacts(context: context) - } : nil), directionHint: entry.directionHint) + } + case let .HoleEntry(_, theme): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) + case let .GroupReferenceEntry(groupReferenceEntry): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem( + presentationData: groupReferenceEntry.presentationData, + context: context, + chatListLocation: location, + filterData: filterData, + index: groupReferenceEntry.index, + content: .groupReference(ChatListItemContent.GroupReferenceData( + groupId: groupReferenceEntry.groupId, + peers: groupReferenceEntry.peers, + message: groupReferenceEntry.message, + unreadCount: groupReferenceEntry.unreadCount, + hiddenByDefault: groupReferenceEntry.hiddenByDefault, + appearsPinned: groupReferenceEntry.appearsPinned, + storyState: groupReferenceEntry.storyState.flatMap { storyState in + return ChatListItemContent.StoryState( + stats: storyState.stats, + hasUnseenCloseFriends: storyState.hasUnseenCloseFriends + ) + } + )), + editing: groupReferenceEntry.editing, + hasActiveRevealControls: false, + selected: false, + header: nil, + enabledContextActions: .auto, + hiddenOffset: groupReferenceEntry.hiddenByDefault && !groupReferenceEntry.revealed, + interaction: nodeInteraction + ), directionHint: entry.directionHint) + case let .ContactEntry(contactEntry): + let header: ChatListSearchItemHeader? = nil + + var status: ContactsPeerItemStatus = .none + status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat) + + let presentationData = contactEntry.presentationData + + let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer) + + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch(isSavedMessages: false), + peer: peerContent, + status: status, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: header, + action: { _ in + nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil, false) + }, + disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) + case let .ArchiveIntro(presentationData): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) + case let .EmptyIntro(presentationData): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint) + case let .SectionHeader(presentationData, displayHide): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? { + hideChatListContacts(context: context) + } : nil), directionHint: entry.directionHint) } } } -private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { +private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry], presentationData: ChatListPresentationData) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { case let .PeerEntry(peerEntry): @@ -828,7 +850,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL hiddenOffset: threadInfo?.isHidden == true && !revealed, interaction: nodeInteraction ), directionHint: entry.directionHint) - case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout, displayPresence): + case let .peers(filter, isSelecting, _, _, filters, displayAutoremoveTimeout, displayPresence): let itemPeer = peer.chatOrMonoforumMainPeer var chatPeer: EnginePeer? if let peer = peer.peers[peer.peerId] { @@ -868,7 +890,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL var header: ChatListSearchItemHeader? switch mode { - case let .peers(_, _, additionalCategories, _, _, _): + case let .peers(_, _, additionalCategories, _, _, _, _): if !additionalCategories.isEmpty { let headerType: ChatListSearchItemHeaderType if case .action = additionalCategories[0].appearance { @@ -1066,6 +1088,28 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } : nil), directionHint: entry.directionHint) case .HeaderEntry: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) + case let .TopPeer(_, peer): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch(isSavedMessages: false), + peer: .peer(peer: peer, chatPeer: peer), + status: .none, + requiresPremiumForMessaging: false, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: nil, + action: { _ in + nodeInteraction.peerSelected(peer, nil, nil, nil, false) + }, + disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData): var header: ChatListSearchItemHeader? if case .peerType = mode { @@ -1091,8 +1135,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } } -private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { - return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) +private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, isPremium: Bool, filterData: ChatListItemFilterData?, chatListFilters: [ChatListFilter]?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, presentationData: ChatListPresentationData, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { + return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries, presentationData: presentationData), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: isPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries, presentationData: presentationData), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade) } private final class ChatListOpaqueTransactionState { @@ -1196,7 +1240,7 @@ public final class ChatListNode: ListViewImpl { return nil case let .PeerId(value): return PeerId(value) - case .ThreadId, .GroupId, .ContactId, .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .additionalCategory: + case .ThreadId, .GroupId, .ContactId, .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .additionalCategory, .TopPeer: return nil } } @@ -1326,7 +1370,7 @@ public final class ChatListNode: ListViewImpl { } var isSelecting = false - if case .peers(_, true, _, _, _, _) = mode { + if case .peers(_, true, _, _, _, _, _) = mode { isSelecting = true } @@ -1839,7 +1883,7 @@ public final class ChatListNode: ListViewImpl { let currentRemovingItemId = self.currentRemovingItemId let savedMessagesPeer: Signal - if case let .peers(filter, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil { + if case let .peers(filter, _, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil { savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> map(Optional.init) |> map { peer in @@ -2102,7 +2146,7 @@ public final class ChatListNode: ListViewImpl { case .chatList: isEmpty = false return true - case let .peers(filter, _, _, _, _, _): + case let .peers(filter, _, _, _, _, _, _): guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false } guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false } @@ -2538,9 +2582,10 @@ public final class ChatListNode: ListViewImpl { if accountIsPremium != previousAccountIsPremium.swap(accountIsPremium) { forceAllUpdated = true } + let presentationData = state.presentationData return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode, forceAllUpdated: forceAllUpdated) - |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) }) + |> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, isPremium: accountIsPremium, filterData: filterData, chatListFilters: chatListFilters, mode: mode, isPeerEnabled: isPeerEnabled, presentationData: presentationData, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } @@ -3391,7 +3436,7 @@ public final class ChatListNode: ListViewImpl { } else { break loop } - case .ArchiveIntro, .EmptyIntro, .SectionHeader, .HeaderEntry, .AdditionalCategory: + case .ArchiveIntro, .EmptyIntro, .SectionHeader, .HeaderEntry, .AdditionalCategory, .TopPeer: break } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 879c0daeb8..d98fddcd84 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -18,6 +18,7 @@ enum ChatListNodeEntryId: Hashable { case SectionHeader case Notice case additionalCategory(Int) + case TopPeer(EnginePeer.Id) } enum ChatListNodeEntrySortIndex: Comparable { @@ -25,6 +26,7 @@ enum ChatListNodeEntrySortIndex: Comparable { case additionalCategory(Int) case sectionHeader case contact(id: EnginePeer.Id, presence: EnginePeer.Presence) + case topPeer(Int) static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool { switch lhs { @@ -34,6 +36,8 @@ enum ChatListNodeEntrySortIndex: Comparable { return lhsIndex < rhsIndex case .additionalCategory: return false + case .topPeer: + return false case .sectionHeader: return true case .contact: @@ -43,6 +47,8 @@ enum ChatListNodeEntrySortIndex: Comparable { switch rhs { case let .additionalCategory(rhsIndex): return lhsIndex < rhsIndex + case .topPeer: + return true case .index: return true case .sectionHeader: @@ -50,9 +56,18 @@ enum ChatListNodeEntrySortIndex: Comparable { case .contact: return true } + case let .topPeer(lhsIndex): + switch rhs { + case .additionalCategory: + return false + case let .topPeer(rhsIndex): + return lhsIndex < rhsIndex + case .index, .sectionHeader, .contact: + return true + } case .sectionHeader: switch rhs { - case .additionalCategory, .index, .sectionHeader: + case .additionalCategory, .topPeer, .index, .sectionHeader: return false case .contact: return true @@ -405,6 +420,7 @@ enum ChatListNodeEntry: Comparable, Identifiable { case EmptyIntro(presentationData: ChatListPresentationData) case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool) case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData) + case TopPeer(index: Int, peer: EnginePeer) var sortIndex: ChatListNodeEntrySortIndex { switch self { @@ -426,6 +442,8 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .sectionHeader case let .AdditionalCategory(index, _, _, _, _, _, _): return .additionalCategory(index) + case let .TopPeer(index, _): + return .topPeer(index) } } @@ -454,6 +472,8 @@ enum ChatListNodeEntry: Comparable, Identifiable { return .SectionHeader case let .AdditionalCategory(_, id, _, _, _, _, _): return .additionalCategory(id) + case let .TopPeer(_, peer): + return .TopPeer(peer.id) } } @@ -551,6 +571,12 @@ enum ChatListNodeEntry: Comparable, Identifiable { } else { return false } + case let .TopPeer(index, peer): + if case .TopPeer(index, peer) = rhs { + return true + } else { + return false + } } } } @@ -930,12 +956,18 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, } if !view.hasLater { - if case let .peers(_, _, additionalCategories, _, _, _) = mode { + if case let .peers(_, _, additionalCategories, topPeers, _, _, _) = mode { var index = 0 for category in additionalCategories.reversed() { result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData)) index += 1 } + + index = 0 + for topPeer in topPeers { + result.append(.TopPeer(index: index, peer: topPeer)) + index += 1 + } } else if case let .peerType(types, hasCreate) = mode, !result.isEmpty && hasCreate { for type in types { switch type { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index 6d5c9bb67a..860ba6ad33 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -25,6 +25,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon private let filter: ChatListNodePeersFilter private let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)? private let selectForumThreads: Bool + private let suggestedPeers: [EnginePeer] private let attemptSelection: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)? private let createNewGroup: (() -> Void)? @@ -114,6 +115,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.immediatelySwitchToContacts = params.immediatelySwitchToContacts self.immediatelyActivateMultipleSelection = params.immediatelyActivateMultipleSelection self.multipleSelectionLimit = params.multipleSelectionLimit + self.suggestedPeers = params.suggestedPeers super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) @@ -263,7 +265,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon override public func loadDisplayNode() { self.navigationBar?.secondaryContentHeight = 44.0 + 10.0 - self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, present: { [weak self] c, a in + self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, suggestedPeers: self.suggestedPeers, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index b438e2e511..abc5f036a2 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -41,6 +41,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let forwardedMessageIds: [EngineMessage.Id] private let hasTypeHeaders: Bool private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? + private let suggestedPeers: [EnginePeer] private var presentationInterfaceState: ChatPresentationInterfaceState private let presentationInterfaceStatePromise = ValuePromise() @@ -115,7 +116,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return (self.presentationData, self.presentationDataPromise.get()) } - init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, suggestedPeers: [EnginePeer], present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context self.controller = controller self.present = present @@ -127,6 +128,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.forwardedMessageIds = forwardedMessageIds self.hasTypeHeaders = hasTypeHeaders self.requestPeerType = requestPeerType + self.suggestedPeers = suggestedPeers self.presentationData = presentationData @@ -209,7 +211,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let requestPeerType = self.requestPeerType { chatListMode = .peerType(type: requestPeerType, hasCreate: hasCreation) } else { - chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false) + chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, topPeers: self.suggestedPeers, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false) } if hasFilters { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index d935fec442..137ec4fb05 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -46,6 +46,8 @@ swift_library( "//submodules/SegmentedControlNode", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/HorizontalTabsComponent", + "//submodules/TelegramUI/Components/LiquidLens", "//submodules/TelegramUI/Components/EdgeEffect", ], visibility = [ diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift index 1b80afc5b6..89a1494604 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift @@ -704,43 +704,46 @@ final class StorageFileListPanelComponent: Component { private struct ItemLayout: Equatable { let containerInsets: UIEdgeInsets let containerWidth: CGFloat + let sideInset: CGFloat let itemHeight: CGFloat let itemCount: Int - + let contentHeight: CGFloat - + init( containerInsets: UIEdgeInsets, containerWidth: CGFloat, + sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int ) { self.containerInsets = containerInsets self.containerWidth = containerWidth + self.sideInset = sideInset self.itemHeight = itemHeight self.itemCount = itemCount - + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight } - + func visibleItems(for rect: CGRect) -> Range? { let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) minVisibleRow = max(0, minVisibleRow) let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) - + let minVisibleIndex = minVisibleRow let maxVisibleIndex = maxVisibleRow - + if maxVisibleIndex >= minVisibleIndex { return minVisibleIndex ..< (maxVisibleIndex + 1) } else { return nil } } - + func itemFrame(for index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + return CGRect(origin: CGPoint(x: self.sideInset, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth - self.sideInset * 2.0, height: self.itemHeight)) } } @@ -752,37 +755,48 @@ final class StorageFileListPanelComponent: Component { class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl - + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView + private let measureItem = ComponentView() private var visibleItems: [EngineMessage.Id: ComponentView] = [:] - + private var ignoreScrolling: Bool = false - + private var component: StorageFileListPanelComponent? private var environment: StorageUsagePanelEnvironment? private var itemLayout: ItemLayout? - + override init(frame: CGRect) { self.scrollView = ScrollViewImpl() - + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate) + super.init(frame: frame) - + + self.addSubview(self.listBackgroundView) + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) + self.addSubview(self.listMaskView) } required init?(coder: NSCoder) { @@ -792,13 +806,44 @@ final class StorageFileListPanelComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) + self.updateListBackground(transition: .immediate) } } - + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) } - + + private func updateListBackground(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + let size = CGSize(width: itemLayout.containerWidth, height: self.scrollView.bounds.height) + guard size.width != 0.0 else { + return + } + + let topInset = itemLayout.containerInsets.top + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + + let scrollingOffset = self.scrollView.contentOffset.y + distanceToTop = -scrollingOffset + topInset + + let itemsContentHeight = self.scrollView.contentSize.height - itemLayout.containerInsets.bottom + let contentBottomOffset = itemsContentHeight - scrollingOffset - self.scrollView.bounds.height + distanceToBottom = -contentBottomOffset + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height)) + transition.setFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.setFrame(view: self.listMaskView, frame: listMaskFrame) + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return @@ -975,7 +1020,7 @@ final class StorageFileListPanelComponent: Component { contextAction: component.contextAction )), environment: {}, - containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.sideInset * 2.0, height: itemLayout.itemHeight) ) let itemFrame = itemLayout.itemFrame(for: index) if let itemComponentView = itemView.view { @@ -1008,6 +1053,9 @@ final class StorageFileListPanelComponent: Component { let environment = environment[StorageUsagePanelEnvironment.self].value self.environment = environment + + self.listBackgroundView.tintColor = environment.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = environment.theme.list.blocksBackgroundColor let measureItemSize = self.measureItem.update( transition: .immediate, @@ -1034,6 +1082,7 @@ final class StorageFileListPanelComponent: Component { let itemLayout = ItemLayout( containerInsets: environment.containerInsets, containerWidth: availableSize.width, + sideInset: 16.0, itemHeight: measureItemSize.height, itemCount: component.items?.items.count ?? 0 ) @@ -1060,7 +1109,8 @@ final class StorageFileListPanelComponent: Component { } self.ignoreScrolling = false self.updateScrolling(transition: transition) - + self.updateListBackground(transition: transition) + return availableSize } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift index 818ac08da7..08200ed5ad 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -454,43 +454,46 @@ final class StoragePeerListPanelComponent: Component { private struct ItemLayout: Equatable { let containerInsets: UIEdgeInsets let containerWidth: CGFloat + let sideInset: CGFloat let itemHeight: CGFloat let itemCount: Int - + let contentHeight: CGFloat - + init( containerInsets: UIEdgeInsets, containerWidth: CGFloat, + sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int ) { self.containerInsets = containerInsets self.containerWidth = containerWidth + self.sideInset = sideInset self.itemHeight = itemHeight self.itemCount = itemCount - + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight } - + func visibleItems(for rect: CGRect) -> Range? { let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) minVisibleRow = max(0, minVisibleRow) let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) - + let minVisibleIndex = minVisibleRow let maxVisibleIndex = maxVisibleRow - + if maxVisibleIndex >= minVisibleIndex { return minVisibleIndex ..< (maxVisibleIndex + 1) } else { return nil } } - + func itemFrame(for index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + return CGRect(origin: CGPoint(x: self.sideInset, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth - self.sideInset * 2.0, height: self.itemHeight)) } } @@ -502,7 +505,9 @@ final class StoragePeerListPanelComponent: Component { class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollViewImpl - + private let listBackgroundView: UIImageView + private let listMaskView: UIImageView + private let measureItem = ComponentView() private var visibleItems: [EnginePeer.Id: ComponentView] = [:] @@ -514,25 +519,35 @@ final class StoragePeerListPanelComponent: Component { override init(frame: CGRect) { self.scrollView = ScrollViewImpl() - + + self.listBackgroundView = UIImageView() + self.listBackgroundView.image = generateStretchableFilledCircleImage(diameter: 26.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) + self.listMaskView = UIImageView() + self.listMaskView.image = generateImage(CGSize(width: 16.0 + 26.0 * 2.0 + 16.0, height: 26.0 * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0))) + })?.stretchableImage(withLeftCapWidth: 16 + 26, topCapHeight: 26).withRenderingMode(.alwaysTemplate) + super.init(frame: frame) - + + self.addSubview(self.listBackgroundView) + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) + self.addSubview(self.listMaskView) } required init?(coder: NSCoder) { @@ -542,13 +557,44 @@ final class StoragePeerListPanelComponent: Component { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) + self.updateListBackground(transition: .immediate) } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) } - + + private func updateListBackground(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + let size = CGSize(width: itemLayout.containerWidth, height: self.scrollView.bounds.height) + guard size.width != 0.0 else { + return + } + + let topInset = itemLayout.containerInsets.top + + var distanceToTop: CGFloat = -100.0 + var distanceToBottom: CGFloat = -100.0 + + let scrollingOffset = self.scrollView.contentOffset.y + distanceToTop = -scrollingOffset + topInset + + let itemsContentHeight = self.scrollView.contentSize.height - itemLayout.containerInsets.bottom + let contentBottomOffset = itemsContentHeight - scrollingOffset - self.scrollView.bounds.height + distanceToBottom = -contentBottomOffset + + distanceToTop = max(-100.0, distanceToTop) + distanceToBottom = max(-100.0, distanceToBottom) + + let listBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: distanceToTop), size: CGSize(width: max(1.0, size.width - 16.0 * 2.0), height: max(1.0, size.height - distanceToBottom - distanceToTop))) + let listMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: listBackgroundFrame.minY), size: CGSize(width: listBackgroundFrame.width + 16.0 * 2.0, height: listBackgroundFrame.height)) + transition.setFrame(view: self.listBackgroundView, frame: listBackgroundFrame) + transition.setFrame(view: self.listMaskView, frame: listMaskFrame) + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { return @@ -607,7 +653,7 @@ final class StoragePeerListPanelComponent: Component { contextAction: component.contextAction )), environment: {}, - containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.sideInset * 2.0, height: itemLayout.itemHeight) ) let itemFrame = itemLayout.itemFrame(for: index) if let itemComponentView = itemView.view { @@ -640,6 +686,9 @@ final class StoragePeerListPanelComponent: Component { let environment = environment[StorageUsagePanelEnvironment.self].value self.environment = environment + + self.listBackgroundView.tintColor = environment.theme.list.itemBlocksBackgroundColor + self.listMaskView.tintColor = environment.theme.list.blocksBackgroundColor let measureItemSize = self.measureItem.update( transition: .immediate, @@ -664,6 +713,7 @@ final class StoragePeerListPanelComponent: Component { let itemLayout = ItemLayout( containerInsets: environment.containerInsets, containerWidth: availableSize.width, + sideInset: 16.0, itemHeight: measureItemSize.height, itemCount: component.items?.items.count ?? 0 ) @@ -690,7 +740,8 @@ final class StoragePeerListPanelComponent: Component { } self.ignoreScrolling = false self.updateScrolling(transition: transition) - + self.updateListBackground(transition: transition) + return availableSize } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift index 57576d65b8..1c452ecd59 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -4,6 +4,8 @@ import Display import ComponentFlow import ComponentDisplayAdapters import TelegramPresentationData +import HorizontalTabsComponent +import GlassBackgroundComponent final class StorageUsagePanelContainerEnvironment: Equatable { let isScrollable: Bool @@ -63,264 +65,6 @@ final class StorageUsagePanelEnvironment: Equatable { } } -private final class StorageUsageHeaderItemComponent: CombinedComponent { - let theme: PresentationTheme - let title: String - let activityFraction: CGFloat - - init( - theme: PresentationTheme, - title: String, - activityFraction: CGFloat - ) { - self.theme = theme - self.title = title - self.activityFraction = activityFraction - } - - static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.activityFraction != rhs.activityFraction { - return false - } - return true - } - - static var body: Body { - let activeText = Child(Text.self) - let inactiveText = Child(Text.self) - - return { context in - let activeText = activeText.update( - component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor), - availableSize: context.availableSize, - transition: .immediate - ) - let inactiveText = inactiveText.update( - component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor), - availableSize: context.availableSize, - transition: .immediate - ) - - context.add(activeText - .position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5)) - .opacity(context.component.activityFraction) - ) - context.add(inactiveText - .position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5)) - .opacity(1.0 - context.component.activityFraction) - ) - - return activeText.size - } - } -} - -private final class StorageUsageHeaderComponent: Component { - struct Item: Equatable { - let id: AnyHashable - let title: String - - init( - id: AnyHashable, - title: String - ) { - self.id = id - self.title = title - } - } - - let theme: PresentationTheme - let items: [Item] - let activeIndex: Int - let transitionFraction: CGFloat - let switchToPanel: (AnyHashable) -> Void - - init( - theme: PresentationTheme, - items: [Item], - activeIndex: Int, - transitionFraction: CGFloat, - switchToPanel: @escaping (AnyHashable) -> Void - ) { - self.theme = theme - self.items = items - self.activeIndex = activeIndex - self.transitionFraction = transitionFraction - self.switchToPanel = switchToPanel - } - - static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.items != rhs.items { - return false - } - if lhs.activeIndex != rhs.activeIndex { - return false - } - if lhs.transitionFraction != rhs.transitionFraction { - return false - } - return true - } - - class View: UIView { - private var component: StorageUsageHeaderComponent? - - private var visibleItems: [AnyHashable: ComponentView] = [:] - private let activeItemLayer: SimpleLayer - - override init(frame: CGRect) { - self.activeItemLayer = SimpleLayer() - self.activeItemLayer.cornerRadius = 2.0 - self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - super.init(frame: frame) - - self.layer.addSublayer(self.activeItemLayer) - - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - let point = recognizer.location(in: self) - var closestId: (CGFloat, AnyHashable)? - if self.bounds.contains(point) { - for (id, item) in self.visibleItems { - if let itemView = item.view { - let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX)) - if let closestIdValue = closestId { - if distance < closestIdValue.0 { - closestId = (distance, id) - } - } else { - closestId = (distance, id) - } - } - } - } - if let closestId = closestId, let component = self.component { - component.switchToPanel(closestId.1) - } - } - } - - func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme - - self.component = component - - var validIds = Set() - for i in 0 ..< component.items.count { - let item = component.items[i] - validIds.insert(item.id) - - let itemView: ComponentView - var itemTransition = transition - if let current = self.visibleItems[item.id] { - itemView = current - } else { - itemTransition = .immediate - itemView = ComponentView() - self.visibleItems[item.id] = itemView - } - - let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction - let activityDistance: CGFloat = abs(activeIndex - CGFloat(i)) - - let activityFraction: CGFloat - if activityDistance < 1.0 { - activityFraction = 1.0 - activityDistance - } else { - activityFraction = 0.0 - } - - let itemSize = itemView.update( - transition: itemTransition, - component: AnyComponent(StorageUsageHeaderItemComponent( - theme: component.theme, - title: item.title, - activityFraction: activityFraction - )), - environment: {}, - containerSize: availableSize - ) - - let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count) - let itemX: CGFloat - if component.items.count == 1 { - itemX = 37.0 - } else { - itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0) - } - - let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize) - if let itemComponentView = itemView.view { - if itemComponentView.superview == nil { - self.addSubview(itemComponentView) - itemComponentView.isUserInteractionEnabled = false - } - itemTransition.setFrame(view: itemComponentView, frame: itemFrame) - } - } - - if component.activeIndex < component.items.count { - let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view - let nextIndex: Int - if component.transitionFraction > 0.0 { - nextIndex = max(0, component.activeIndex - 1) - } else { - nextIndex = min(component.items.count - 1, component.activeIndex + 1) - } - let nextView = self.visibleItems[component.items[nextIndex].id]?.view - if let activeView = activeView, let nextView = nextView { - let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction)) - transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0))) - } - } - - if themeUpdated { - self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor - } - - var removeIds: [AnyHashable] = [] - for (id, itemView) in self.visibleItems { - if !validIds.contains(id) { - removeIds.append(id) - if let itemComponentView = itemView.view { - itemComponentView.removeFromSuperview() - } - } - } - for id in removeIds { - self.visibleItems.removeValue(forKey: id) - } - - return availableSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - final class StorageUsagePanelContainerComponent: Component { typealias EnvironmentType = StorageUsagePanelContainerEnvironment @@ -383,10 +127,9 @@ final class StorageUsagePanelContainerComponent: Component { } class View: UIView, UIGestureRecognizerDelegate { - private let topPanelBackgroundView: UIView - private let topPanelMergedBackgroundView: UIView - private let topPanelSeparatorLayer: SimpleLayer - private let header = ComponentView() + private let tabsBackgroundContainer: GlassBackgroundContainerView + private let tabsBackgroundView: GlassBackgroundView + private let tabsContainer = ComponentView() private var component: StorageUsagePanelContainerComponent? private weak var state: EmptyComponentState? @@ -396,25 +139,21 @@ final class StorageUsagePanelContainerComponent: Component { private var actualVisibleIds = Set() private var currentId: AnyHashable? private var transitionFraction: CGFloat = 0.0 + private var isDraggingTabs: Bool = false private var animatingTransition: Bool = false override init(frame: CGRect) { - self.topPanelBackgroundView = UIView() - - self.topPanelMergedBackgroundView = UIView() - self.topPanelMergedBackgroundView.alpha = 0.0 - - self.topPanelSeparatorLayer = SimpleLayer() - + self.tabsBackgroundContainer = GlassBackgroundContainerView() + self.tabsBackgroundView = GlassBackgroundView() + self.panelsBackgroundLayer = SimpleLayer() - + super.init(frame: frame) - + self.layer.addSublayer(self.panelsBackgroundLayer) - self.addSubview(self.topPanelBackgroundView) - self.addSubview(self.topPanelMergedBackgroundView) - self.layer.addSublayer(self.topPanelSeparatorLayer) - + self.tabsBackgroundContainer.contentView.addSubview(self.tabsBackgroundView) + self.addSubview(self.tabsBackgroundContainer) + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in guard let self, let component = self.component, let currentId = self.currentId else { return [] @@ -422,11 +161,7 @@ final class StorageUsagePanelContainerComponent: Component { guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { return [] } - - /*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { - return [] - }*/ - + if index == 0 { return .left } @@ -480,7 +215,8 @@ final class StorageUsagePanelContainerComponent: Component { } cancelContextGestures(view: self) - + self.isDraggingTabs = true + //self.animatingTransition = true case .changed: guard let component = self.component, let currentId = self.currentId else { @@ -535,6 +271,7 @@ final class StorageUsagePanelContainerComponent: Component { component.currentPanelUpdated(currentId, transition) } + self.isDraggingTabs = false self.animatingTransition = false //self.currentPaneUpdated?(false) @@ -545,8 +282,16 @@ final class StorageUsagePanelContainerComponent: Component { } func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) { - transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value) - transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let component = self.component else { + return nil + } + if point.y < component.insets.top { + return nil + } + return super.hitTest(point, with: event) } func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -558,34 +303,30 @@ final class StorageUsagePanelContainerComponent: Component { self.state = state if themeUpdated { - self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor - self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor - self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor - self.topPanelMergedBackgroundView.backgroundColor = component.theme.list.blocksBackgroundColor + //self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor } - let topPanelCoverHeight: CGFloat = 10.0 - - let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0)) - transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) - transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame) - - transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))) - - transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - + let tabsHeight: CGFloat = 40.0 + let tabsTopInset: CGFloat = component.insets.top + 10.0 + let tabsBottomInset: CGFloat = 10.0 + let tabsSideInset: CGFloat = 16.0 + component.insets.left + + let tabsContainerSize = CGSize(width: availableSize.width - tabsSideInset * 2.0, height: tabsHeight) + + transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: component.insets.top), size: CGSize(width: availableSize.width, height: availableSize.height - component.insets.top))) + if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { self.currentId = nil } if self.currentId == nil { self.currentId = component.items.first?.id } - + var visibleIds = Set() var currentIndex: Int? if let currentId = self.currentId { visibleIds.insert(currentId) - + if let index = component.items.firstIndex(where: { $0.id == currentId }) { currentIndex = index if index != 0 { @@ -596,50 +337,73 @@ final class StorageUsagePanelContainerComponent: Component { } } } - - let _ = self.header.update( + + let tabsContainerEffectiveSize = self.tabsContainer.update( transition: transition, - component: AnyComponent(StorageUsageHeaderComponent( + component: AnyComponent(HorizontalTabsComponent( + context: nil, theme: component.theme, - items: component.items.map { item -> StorageUsageHeaderComponent.Item in - return StorageUsageHeaderComponent.Item( + tabs: component.items.map { item -> HorizontalTabsComponent.Tab in + return HorizontalTabsComponent.Tab( id: item.id, - title: item.title + content: .title(HorizontalTabsComponent.Tab.Title(text: item.title, entities: [], enableAnimations: false)), + badge: nil, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + if component.items.contains(where: { $0.id == item.id }) { + self.currentId = item.id + let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + component.currentPanelUpdated(item.id, transition) + } + } ) }, - activeIndex: currentIndex ?? 0, - transitionFraction: self.transitionFraction, - switchToPanel: { [weak self] id in - guard let self, let component = self.component else { - return - } - if component.items.contains(where: { $0.id == id }) { - self.currentId = id - let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)) - self.state?.updated(transition: transition) - component.currentPanelUpdated(id, transition) - } - } + selectedTab: self.currentId, + isEditing: false, + layout: .fit, + liftWhileSwitching: true )), environment: {}, - containerSize: topPanelFrame.size + containerSize: tabsContainerSize ) - if let headerView = self.header.view { - if headerView.superview == nil { - self.addSubview(headerView) + + let tabContainerFrame = CGRect( + origin: CGPoint( + x: floorToScreenPixels((availableSize.width - tabsContainerEffectiveSize.width) / 2.0), + y: tabsTopInset + ), + size: tabsContainerEffectiveSize + ) + + transition.setFrame(view: self.tabsBackgroundContainer, frame: tabContainerFrame) + self.tabsBackgroundContainer.update(size: tabContainerFrame.size, isDark: component.theme.overallDarkAppearance, transition: transition) + + transition.setFrame(view: self.tabsBackgroundView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size)) + self.tabsBackgroundView.update(size: tabContainerFrame.size, cornerRadius: tabContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) + + if let tabsContainerView = self.tabsContainer.view as? HorizontalTabsComponent.View { + if tabsContainerView.superview == nil { + self.tabsBackgroundView.contentView.addSubview(tabsContainerView) + tabsContainerView.setOverlayContainerView(overlayContainerView: self) } - transition.setFrame(view: headerView, frame: topPanelFrame) + transition.setFrame(view: tabsContainerView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size)) + tabsContainerView.updateTabSwitchFraction(fraction: self.transitionFraction, isDragging: self.isDraggingTabs, transition: transition) } + + let effectiveTabsHeight = tabsTopInset + tabContainerFrame.height + tabsBottomInset let childEnvironment = StorageUsagePanelEnvironment( theme: component.theme, strings: component.strings, dateTimeFormat: component.dateTimeFormat, - containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right), + containerInsets: UIEdgeInsets(top: effectiveTabsHeight, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right), isScrollable: environment.isScrollable ) - let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)) + let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)) if self.animatingTransition { visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil }) @@ -721,7 +485,7 @@ final class StorageUsagePanelContainerComponent: Component { ) if let panelView = panel.view { if panelView.superview == nil { - self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + self.insertSubview(panelView, belowSubview: self.tabsBackgroundContainer) } panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 827f5feb2d..a7c10155d9 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -1122,8 +1122,8 @@ final class StorageUsageScreenComponent: Component { } var wasLockedAtPanels = false - if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { - if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + if let panelContainerView = self.panelContainer.view { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - 0.0)) <= UIScreenPixel { wasLockedAtPanels = true } } @@ -2214,7 +2214,7 @@ final class StorageUsageScreenComponent: Component { theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + insets: UIEdgeInsets(top: environment.navigationHeight, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), items: panelItems, currentPanelUpdated: { [weak self] id, transition in guard let self else { @@ -2227,20 +2227,20 @@ final class StorageUsageScreenComponent: Component { environment: { StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels) }, - containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) if let panelContainerView = self.panelContainer.view { if panelContainerView.superview == nil { self.scrollContainerView.addSubview(panelContainerView) } - transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - environment.navigationHeight), size: panelContainerSize)) if self.topContentOverlayView.superview == nil { self.scrollContainerView.insertSubview(self.topContentOverlayView, belowSubview: panelContainerView) } self.topContentOverlayView.backgroundColor = environment.theme.list.blocksBackgroundColor transition.setFrame(view: self.topContentOverlayView, frame: CGRect(origin: CGPoint(x: 0.0, y: panelContainerView.frame.minY - availableSize.height), size: availableSize)) } - contentHeight += panelContainerSize.height + contentHeight += panelContainerSize.height - environment.navigationHeight } else { self.panelContainer.view?.removeFromSuperview() self.topContentOverlayView.removeFromSuperview() @@ -2259,7 +2259,7 @@ final class StorageUsageScreenComponent: Component { var scrollViewBounds = self.scrollView.bounds scrollViewBounds.size = availableSize if wasLockedAtPanels, let panelContainerView = self.panelContainer.view { - scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight + scrollViewBounds.origin.y = panelContainerView.frame.minY } transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index d09670712e..fc854dfdb9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -554,85 +554,96 @@ func presentChatReplyOptions(selfController: ChatControllerImpl, sourceView: UIV func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) { let _ = selfController.presentVoiceMessageDiscardAlert(action: { [weak selfController] in - guard let selfController else { - return - } - let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages] - var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? - let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( - context: selfController.context, - updatedPresentationData: selfController.updatedPresentationData, - filter: filter, - hasFilters: true, - title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle, - attemptSelection: { peer, _, reason in - attemptSelectionImpl?(peer, reason) - }, - multipleSelection: false, - forwardedMessageIds: [], - selectForumThreads: true - )) - let context = selfController.context - attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in - guard let selfController, let controller = controller else { + Task { @MainActor in + guard let selfController else { return } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - switch reason { - case .generic: - controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - case .premiumRequired: - controller.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() + let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages] + var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? + + var suggestedPeers: [EnginePeer] = [] + if let message = await selfController.context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.Message(id: replySubject.messageId) + ).get(), case let .user(author) = message.author { + suggestedPeers.append(.user(author)) + } + + let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams( + context: selfController.context, + updatedPresentationData: selfController.updatedPresentationData, + filter: filter, + hasFilters: true, + title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle, + attemptSelection: { peer, _, reason in + attemptSelectionImpl?(peer, reason) + }, + multipleSelection: false, + forwardedMessageIds: [], + selectForumThreads: true, + suggestedPeers: suggestedPeers + )) + let context = selfController.context + attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in + guard let selfController, let controller = controller else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + switch reason { + case .generic: + controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + case .premiumRequired: + controller.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true } - return true - } - - var hasAction = false - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: selfController.context.currentAppConfiguration.with { $0 }) - if !premiumConfiguration.isPremiumDisabled { - hasAction = true - } - - controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: hasAction ? presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action : nil, timeout: nil, linkAction: { _ in - }), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in - guard let selfController, let controller else { + + var hasAction = false + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: selfController.context.currentAppConfiguration.with { $0 }) + if !premiumConfiguration.isPremiumDisabled { + hasAction = true + } + + controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: hasAction ? presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action : nil, timeout: nil, linkAction: { _ in + }), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in + guard let selfController, let controller else { + return false + } + if case .undo = action { + let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings) + controller.push(premiumController) + } return false - } - if case .undo = action { - let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings) - controller.push(premiumController) - } - return false - }), in: .current) + }), in: .current) + } } + controller.peerSelected = { [weak selfController, weak controller] peer, threadId in + guard let selfController, let strongController = controller else { + return + } + let peerId = peer.id + //let accountPeerId = selfController.context.account.peerId + + var isPinnedMessages = false + if case .pinnedMessages = selfController.presentationInterfaceState.subject { + isPinnedMessages = true + } + + if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages { + selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) }) + selfController.updateItemNodesSearchTextHighlightStates() + selfController.searchResultsController = nil + strongController.dismiss() + } else { + moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in + strongController?.dismiss() + }) + } + } + selfController.chatDisplayNode.dismissInput() + selfController.effectiveNavigationController?.pushViewController(controller) } - controller.peerSelected = { [weak selfController, weak controller] peer, threadId in - guard let selfController, let strongController = controller else { - return - } - let peerId = peer.id - //let accountPeerId = selfController.context.account.peerId - - var isPinnedMessages = false - if case .pinnedMessages = selfController.presentationInterfaceState.subject { - isPinnedMessages = true - } - - if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages { - selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) }) - selfController.updateItemNodesSearchTextHighlightStates() - selfController.searchResultsController = nil - strongController.dismiss() - } else { - moveReplyToChat(selfController: selfController, peerId: peerId, threadId: threadId, replySubject: replySubject, completion: { [weak strongController] in - strongController?.dismiss() - }) - } - } - selfController.chatDisplayNode.dismissInput() - selfController.effectiveNavigationController?.pushViewController(controller) }) } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 08c8d27e4d..ba2ffa3754 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -189,7 +189,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } placeholder = placeholderValue - let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) + let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), chatListFilter: chatListFilter, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], topPeers: [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) chatListNode.passthroughPeerSelection = true chatListNode.disabledPeerSelected = { peer, _, reason in attemptDisabledItemSelection?(peer, reason)