diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 49eccbd317..8c92b1ef94 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1763,7 +1763,7 @@ private final class NotificationServiceHandler { |> mapToSignal { content, _ -> Signal<(NotificationContent, Media?), NoError> in return stateManager.postbox.transaction { transaction -> (NotificationContent, Media?) in var parsedMedia: Media? - if let messageId, let message = transaction.getMessage(messageId), !message.containsSecretMedia { + if let messageId, let message = transaction.getMessage(messageId), !message.containsSecretMedia, !message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) { if let media = message.media.first { parsedMedia = media } diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift index 4fe507f2f1..c96f0dd597 100644 --- a/submodules/ComposePollUI/Sources/ComposePollScreen.swift +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -477,7 +477,7 @@ final class ComposePollScreenComponent: Component { defer { self.isUpdating = false } - + var alphaTransition = transition if !transition.animation.isImmediate { alphaTransition = alphaTransition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) @@ -850,7 +850,7 @@ final class ComposePollScreenComponent: Component { } } - if self.pollOptions.count < 10, let lastOption = self.pollOptions.last { + if self.pollOptions.count < component.initialData.maxPollAnswersCount, let lastOption = self.pollOptions.last { if lastOption.textInputState.text.length != 0 { self.pollOptions.append(PollOption(id: self.nextPollOptionId)) self.nextPollOptionId += 1 @@ -921,7 +921,7 @@ final class ComposePollScreenComponent: Component { contentHeight += 7.0 - let pollOptionsLimitReached = self.pollOptions.count >= 10 + let pollOptionsLimitReached = self.pollOptions.count >= component.initialData.maxPollAnswersCount var animatePollOptionsFooterIn = false var pollOptionsFooterTransition = transition if self.currentPollOptionsLimitReached != pollOptionsLimitReached { @@ -944,7 +944,7 @@ final class ComposePollScreenComponent: Component { maximumNumberOfLines: 0 )) } else { - let remainingCount = 10 - self.pollOptions.count + let remainingCount = component.initialData.maxPollAnswersCount - self.pollOptions.count let rawString = environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount)) var pollOptionsFooterItems: [AnimatedTextComponent.Item] = [] @@ -1476,13 +1476,16 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont public final class InitialData { fileprivate let maxPollTextLength: Int fileprivate let maxPollOptionLength: Int + fileprivate let maxPollAnswersCount: Int fileprivate init( maxPollTextLength: Int, - maxPollOptionLength: Int + maxPollOptionLength: Int, + maxPollAnwsersCount: Int ) { self.maxPollTextLength = maxPollTextLength self.maxPollOptionLength = maxPollOptionLength + self.maxPollAnswersCount = maxPollAnwsersCount } } @@ -1577,9 +1580,14 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont } public static func initialData(context: AccountContext) -> InitialData { + var maxPollAnwsersCount: Int = 10 + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_answers_max"] as? Double { + maxPollAnwsersCount = Int(value) + } return InitialData( maxPollTextLength: Int(200), - maxPollOptionLength: 100 + maxPollOptionLength: 100, + maxPollAnwsersCount: maxPollAnwsersCount ) } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index fe89cebbd1..4be8b9e67c 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -426,7 +426,9 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis let text = strings.Contacts_PermissionsText switch authorizationStatus { case .limited: - entries.append(.permissionLimited(theme, strings)) + if displaySortOptions { + entries.append(.permissionLimited(theme, strings)) + } case .denied: entries.append(.permissionInfo(theme, title, text, suppressed)) entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0)) diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 0c381ea128..32392fb45d 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -881,16 +881,6 @@ open class NavigationBar: ASDisplayNode { if needsLeftButton { if animated { - if self.leftButtonNode.view.superview != nil { - if let snapshotView = self.leftButtonNode.view.snapshotContentTree() { - snapshotView.frame = self.leftButtonNode.frame - self.leftButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.leftButtonNode.view) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - } - if self.backButtonNode.view.superview != nil { if let snapshotView = self.backButtonNode.view.snapshotContentTree() { snapshotView.frame = self.backButtonNode.frame @@ -927,9 +917,11 @@ open class NavigationBar: ASDisplayNode { self.badgeNode.removeFromSupernode() if let leftBarButtonItem = item.leftBarButtonItem { - self.leftButtonNode.updateItems([leftBarButtonItem]) + self.leftButtonNode.updateItems([], animated: animated) + self.leftButtonNode.updateItems([leftBarButtonItem], animated: animated) } else { - self.leftButtonNode.updateItems([UIBarButtonItem(title: self.presentationData.strings.close, style: .plain, target: nil, action: nil)]) + self.leftButtonNode.updateItems([], animated: animated) + self.leftButtonNode.updateItems([UIBarButtonItem(title: self.presentationData.strings.close, style: .plain, target: nil, action: nil)], animated: animated) } if self.leftButtonNode.supernode == nil { @@ -994,9 +986,6 @@ open class NavigationBar: ASDisplayNode { } self.updateAccessibilityElements() - if animated { - self.hintAnimateTitleNodeOnNextLayout = true - } } private func updateRightButton(animated: Bool) { @@ -1008,23 +997,14 @@ open class NavigationBar: ASDisplayNode { items = [rightBarButtonItem] } + self.rightButtonNodeUpdated = true + if !items.isEmpty { - if animated, self.rightButtonNode.view.superview != nil { - if let snapshotView = self.rightButtonNode.view.snapshotContentTree() { - snapshotView.frame = self.rightButtonNode.frame - self.rightButtonNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.rightButtonNode.view) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - } - self.rightButtonNode.updateItems(items) + self.rightButtonNode.updateItems([], animated: animated) + self.rightButtonNode.updateItems(items, animated: animated) if self.rightButtonNode.supernode == nil { self.buttonsContainerNode.addSubnode(self.rightButtonNode) } - if animated { - self.rightButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } } else { if animated, self.rightButtonNode.view.superview != nil { if let snapshotView = self.rightButtonNode.view.snapshotContentTree() { @@ -1050,9 +1030,6 @@ open class NavigationBar: ASDisplayNode { self.rightButtonNode.removeFromSupernode() } - if animated { - self.hintAnimateTitleNodeOnNextLayout = true - } self.updateAccessibilityElements() } @@ -1062,6 +1039,7 @@ open class NavigationBar: ASDisplayNode { public let backButtonArrow: ASImageNode public let leftButtonNode: NavigationButtonNode public let rightButtonNode: NavigationButtonNode + private var rightButtonNodeUpdated: Bool = false public let additionalContentNode: SparseNode private let navigationBackgroundCutoutView: NavigationBackgroundCutoutView @@ -1359,7 +1337,7 @@ open class NavigationBar: ASDisplayNode { var leftTitleInset: CGFloat = leftInset + 1.0 var rightTitleInset: CGFloat = rightInset + 1.0 if self.backButtonNode.supernode != nil { - let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) + let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape, isLeftAligned: true) leftTitleInset = backButtonSize.width + backButtonInset + 1.0 let topHitTestSlop = (nominalHeight - backButtonSize.height) * 0.5 @@ -1411,7 +1389,7 @@ open class NavigationBar: ASDisplayNode { self.badgeNode.alpha = 1.0 } } else if self.leftButtonNode.supernode != nil { - let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) + let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape, isLeftAligned: true) leftTitleInset = leftButtonSize.width + leftButtonInset + 1.0 var transition = transition @@ -1428,16 +1406,18 @@ open class NavigationBar: ASDisplayNode { transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: backButtonArrowFrame.origin.offsetBy(dx: 16.0, dy: 2.0), size: badgeSize)) if self.rightButtonNode.supernode != nil { - let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape) + let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape, isLeftAligned: false) rightTitleInset = rightButtonSize.width + leftButtonInset + 1.0 self.rightButtonNode.alpha = 1.0 var transition = transition - if self.rightButtonNode.frame.width.isZero { + if self.rightButtonNode.frame.width.isZero || self.rightButtonNodeUpdated { transition = .immediate } + transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize)) } + self.rightButtonNodeUpdated = false if let transitionState = self.transitionState { let progress = transitionState.progress @@ -1447,7 +1427,7 @@ open class NavigationBar: ASDisplayNode { break case .bottom: if let transitionBackButtonNode = self.transitionBackButtonNode { - let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) + let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape, isLeftAligned: true) let initialX: CGFloat = backButtonInset + size.width * 0.3 let finalX: CGFloat = floor((size.width - transitionBackButtonSize.width) / 2.0) @@ -1592,7 +1572,7 @@ open class NavigationBar: ASDisplayNode { node.updateManualText(self.backButtonNode.manualText) node.color = accentColor if let validLayout = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape) + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape, isLeftAligned: true) node.frame = self.backButtonNode.frame } return node @@ -1608,7 +1588,7 @@ open class NavigationBar: ASDisplayNode { node.updateManualText(self.backButtonNode.manualText) node.color = accentColor if let validLayout = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape) + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape, isLeftAligned: true) node.frame = self.backButtonNode.frame } return node.view @@ -1628,10 +1608,10 @@ open class NavigationBar: ASDisplayNode { items = [rightBarButtonItem] } } - node.updateItems(items) + node.updateItems(items, animated: false) node.color = accentColor if let validLayout = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape) + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape, isLeftAligned: false) node.frame = self.backButtonNode.frame } return node diff --git a/submodules/Display/Source/NavigationButtonNode.swift b/submodules/Display/Source/NavigationButtonNode.swift index ea05895c76..73d49fc855 100644 --- a/submodules/Display/Source/NavigationButtonNode.swift +++ b/submodules/Display/Source/NavigationButtonNode.swift @@ -331,6 +331,8 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public final class NavigationButtonNode: ContextControllerSourceNode { private var nodes: [NavigationButtonItemNode] = [] + private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = [] + public var singleCustomNode: ASDisplayNode? { for node in self.nodes { return node.node @@ -452,7 +454,7 @@ public final class NavigationButtonNode: ContextControllerSourceNode { } } - func updateItems(_ items: [UIBarButtonItem]) { + func updateItems(_ items: [UIBarButtonItem], animated: Bool) { for i in 0 ..< items.count { let node: NavigationButtonItemNode if self.nodes.count > i { @@ -486,16 +488,41 @@ public final class NavigationButtonNode: ContextControllerSourceNode { node.bold = items[i].style == .done node.isEnabled = items[i].isEnabled node.node = items[i].customDisplayNode + + if animated { + node.layer.animateAlpha(from: 0.0, to: self.manualAlpha, duration: 0.16) + node.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } } if items.count < self.nodes.count { for i in items.count ..< self.nodes.count { - self.nodes[i].removeFromSupernode() + let itemNode = self.nodes[i] + if animated { + disappearingNodes.append((itemNode.frame, self.bounds.size, itemNode)) + itemNode.layer.animateAlpha(from: self.manualAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak itemNode] _ in + guard let itemNode else { + return + } + + itemNode.removeFromSupernode() + + guard let self else { + return + } + if let index = self.disappearingNodes.firstIndex(where: { $0.node === itemNode }) { + self.disappearingNodes.remove(at: index) + } + }) + itemNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } else { + itemNode.removeFromSupernode() + } } self.nodes.removeSubrange(items.count...) } } - public func updateLayout(constrainedSize: CGSize, isLandscape: Bool) -> CGSize { + public func updateLayout(constrainedSize: CGSize, isLandscape: Bool, isLeftAligned: Bool) -> CGSize { var nodeOrigin = CGPoint() var totalHeight: CGFloat = 0.0 for i in 0 ..< self.nodes.count { @@ -520,6 +547,13 @@ public final class NavigationButtonNode: ContextControllerSourceNode { nodeOrigin.x -= 5.0 } } + + if !isLeftAligned { + for disappearingNode in self.disappearingNodes { + disappearingNode.node.frame = disappearingNode.frame.offsetBy(dx: nodeOrigin.x - disappearingNode.size.width, dy: (totalHeight - disappearingNode.size.height) * 0.5) + } + } + return CGSize(width: nodeOrigin.x, height: totalHeight) } diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 02c50f0ed3..c2727f91cf 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -484,6 +484,17 @@ public extension UIImage { } return result } + + func fixedOrientation() -> UIImage { + if self.imageOrientation == .up { return self } + + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + self.draw(in: CGRect(origin: .zero, size: size)) + let normalizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return normalizedImage ?? self + } } private func makeSubtreeSnapshot(layer: CALayer, keepPortals: Bool = false, keepTransform: Bool = false) -> UIView? { diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 228a9fd417..8590765693 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1259,7 +1259,7 @@ public final class ShareController: ViewController { var result: [EnginePeer.Id: EnginePeer?] = [:] var requiresStars: [EnginePeer.Id: StarsAmount] = [:] for peerId in peerIds { - if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? PeerView, let peer = peerViewMainPeer(view) { + if let view = views.views[PostboxViewKey.peer(peerId: peerId, components: [])] as? PeerView, let peer = peerViewMainPeer(view) { result[peerId] = EnginePeer(peer) if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView { if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData { @@ -1913,7 +1913,7 @@ public final class ShareController: ViewController { var result: [EnginePeer.Id: EnginePeer?] = [:] var requiresStars: [EnginePeer.Id: StarsAmount] = [:] for peerId in peerIds { - if let view = views.views[PostboxViewKey.basicPeer(peerId)] as? PeerView, let peer = peerViewMainPeer(view) { + if let view = views.views[PostboxViewKey.peer(peerId: peerId, components: [])] as? PeerView, let peer = peerViewMainPeer(view) { result[peerId] = EnginePeer(peer) if peer is TelegramUser, let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView { if let cachedData = cachedPeerDataView.cachedPeerData as? CachedUserData { @@ -2549,7 +2549,7 @@ public final class ShareController: ViewController { if let view = views.views[.cachedPeerData(peerId: id)] as? CachedPeerDataView, let data = view.cachedPeerData as? CachedUserData { requiresPremiumForMessaging[id] = data.flags.contains(.premiumRequired) requiresStars[id] = data.sendPaidMessageStars?.value - } else if let view = views.views[.basicPeer(id)] as? PeerView, let channel = peerViewMainPeer(view) as? TelegramChannel { + } else if let view = views.views[.peer(peerId: id, components: [])] as? PeerView, let channel = peerViewMainPeer(view) as? TelegramChannel { requiresStars[id] = channel.sendPaidMessageStars?.value } else { requiresPremiumForMessaging[id] = false diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 8ad523ddd1..1c35635c1b 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -116,6 +116,10 @@ public final class PresentationData: Equatable { return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji) } + public func withUpdate(listsFontSize: PresentationFontSize) -> PresentationData { + return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji) + } + public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool { return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.reduceMotion == rhs.reduceMotion && lhs.largeEmoji == rhs.largeEmoji } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index bb868a76cc..0d7dac3104 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1105,7 +1105,7 @@ public struct PresentationResourcesChat { public static func chatFreeNavigateButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { return theme.image(PresentationResourceKey.chatFreeNavigateButtonIcon.rawValue, { _ in - return generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/NavigateToMessageIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) }) } diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 865e0b55ac..37a7531905 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -265,31 +265,47 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { public final class SnapshotState { fileprivate let snapshotView: UIView? + fileprivate let snapshotStatusView: UIView? - fileprivate init(snapshotView: UIView?) { + fileprivate init(snapshotView: UIView?, snapshotStatusView: UIView?) { self.snapshotView = snapshotView + self.snapshotStatusView = snapshotStatusView } } public func prepareSnapshotState() -> SnapshotState { let snapshotView = self.avatarNode.view.snapshotView(afterScreenUpdates: false) + let snapshotStatusView = self.statusView.view?.snapshotView(afterScreenUpdates: false) return SnapshotState( - snapshotView: snapshotView + snapshotView: snapshotView, + snapshotStatusView: snapshotStatusView ) } public func animateFromSnapshot(_ snapshotState: SnapshotState) { - self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true) + self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16) + self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true) + + self.statusView.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16) + self.statusView.view?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true) if let snapshotView = snapshotState.snapshotView { snapshotView.frame = self.frame - self.containerNode.view.addSubview(snapshotView) + self.containerNode.view.insertSubview(snapshotView, at: 0) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) - snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + if let snapshotStatusView = snapshotState.snapshotStatusView { + snapshotStatusView.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - snapshotStatusView.bounds.width) / 2.0), y: floor((self.containerNode.bounds.height - snapshotStatusView.bounds.height) / 2.0)), size: snapshotStatusView.bounds.size) + self.containerNode.view.insertSubview(snapshotStatusView, at: 0) + + snapshotStatusView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotStatusView] _ in + snapshotStatusView?.removeFromSuperview() + }) + snapshotStatusView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ec521589b0..802dcf7898 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1926,7 +1926,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI inlineBotNameString = attribute.title } } else if let attribute = attribute as? ReplyMessageAttribute { - if case let .replyThread(replyThreadMessage) = item.chatLocation, Int32(clamping: replyThreadMessage.threadId) == attribute.messageId.id { + if let threadId = firstMessage.threadId, Int32(clamping: threadId) == attribute.messageId.id { } else { replyMessage = firstMessage.associatedMessages[attribute.messageId] } @@ -2437,7 +2437,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI headerSize.height += 7.0 } - if isSidePanelOpen { + if isSidePanelOpen && incoming { hasTitleAvatar = true } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 33121fb337..4ed2ba5005 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -531,7 +531,11 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { avatarNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: titleLayout.size.height + titleAuthorSpacing), size: avatarSize) avatarNode.updateSize(size: avatarSize) if let peer { - avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize) + if peer.smallProfileImage != nil { + avatarNode.setPeerV2(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize) + } else { + avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize) + } } else if let authorName, !authorName.isEmpty { avatarNode.setCustomLetters([String(authorName[authorName.startIndex])]) } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 4dc0005076..0a7faa26da 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -322,7 +322,9 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible if let channel = content.firstMessage.peers[content.firstMessage.id.peerId] as? TelegramChannel, channel.isForumOrMonoForum { if case .replyThread = chatLocation { - displayAuthorInfo = false + if channel.isMonoForum && chatLocation.threadId != context.account.peerId.toInt64() { + displayAuthorInfo = false + } } else { if channel.isMonoForum { if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = content.firstMessage.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift index bd516b177b..65c004db87 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift @@ -38,7 +38,7 @@ public class ChatUnreadItem: ListViewItem { Queue.mainQueue().async { completion(node, { return (nil, { _ in - apply() + apply(.None) }) }) } @@ -56,7 +56,7 @@ public class ChatUnreadItem: ListViewItem { let (layout, apply) = nodeLayout(self, params, dateAtBottom) Queue.mainQueue().async { completion(layout, { _ in - apply() + apply(animation) }) } } @@ -122,18 +122,18 @@ public class ChatUnreadItemNode: ListViewItemNode { if let item = item as? ChatUnreadItem { let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) let (layout, apply) = self.asyncLayout()(item, params, dateAtBottom) - apply() + apply(.None) self.contentSize = layout.contentSize self.insets = layout.insets } } - public func asyncLayout() -> (_ item: ChatUnreadItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ChatUnreadItem, _ params: ListViewItemLayoutParams, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants let currentTheme = self.theme - return { item, params, dateAtBottom in + return { [weak self] item, params, dateAtBottom in var updatedBackgroundImage: UIImage? if currentTheme != item.presentationData.theme { updatedBackgroundImage = PresentationResourcesChat.chatUnreadBarBackgroundImage(item.presentationData.theme.theme) @@ -144,7 +144,7 @@ public class ChatUnreadItemNode: ListViewItemNode { let backgroundSize = CGSize(width: params.width, height: 25.0) - return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 25.0), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in + return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 25.0), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { animation in if let strongSelf = self { strongSelf.item = item strongSelf.theme = item.presentationData.theme @@ -159,7 +159,10 @@ public class ChatUnreadItemNode: ListViewItemNode { strongSelf.activateArea.accessibilityLabel = string strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize) - strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size) + + let labelFrame = CGRect(origin: CGPoint(x: params.leftInset + floorToScreenPixels((backgroundSize.width - params.leftInset - params.rightInset - size.size.width) / 2.0), y: floorToScreenPixels((backgroundSize.height - size.size.height) / 2.0)), size: size.size) + animation.animator.updatePosition(layer: strongSelf.labelNode.layer, position: labelFrame.center, completion: nil) + strongSelf.labelNode.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index f7a7d09dcd..5a8b4b2d42 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -1021,16 +1021,16 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { if activitySize.width < size.width { activityFrame.origin.x = -clearBounds.minX + floor((size.width - activityFrame.width) / 2.0) } - self.activityNode.frame = activityFrame + titleTransition.updateFrameAdditiveToCenter(node: self.activityNode, frame: activityFrame) } if let image = self.titleLeftIconNode.image { - self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size) + titleTransition.updateFrame(node: self.titleLeftIconNode, frame: CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size)) } var nextIconX: CGFloat = titleFrame.width - self.titleVerifiedIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((titleFrame.height - titleVerifiedSize.height) / 2.0)), size: titleVerifiedSize) + titleTransition.updateFrame(view: self.titleVerifiedIconView, frame: CGRect(origin: CGPoint(x: 0.0, y: floor((titleFrame.height - titleVerifiedSize.height) / 2.0)), size: titleVerifiedSize)) self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) nextIconX -= titleCredibilitySize.width @@ -1056,7 +1056,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleTransition.updateFrameAdditiveToCenter(view: self.titleContainerView, frame: titleFrame) titleTransition.updateFrameAdditiveToCenter(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) - self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + verifiedIconWidth + statusIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize) + titleTransition.updateFrameAdditiveToCenter(node: self.activityNode, frame: CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + verifiedIconWidth + statusIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize)) if let image = self.titleLeftIconNode.image { self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size) @@ -1069,11 +1069,11 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) nextIconX -= titleCredibilitySize.width - self.titleStatusIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleStatusSize.width, y: floor((titleFrame.height - titleStatusSize.height) / 2.0)), size: titleStatusSize) + titleTransition.updateFrame(view: self.titleStatusIconView, frame: CGRect(origin: CGPoint(x: nextIconX - titleStatusSize.width, y: floor((titleFrame.height - titleStatusSize.height) / 2.0)), size: titleStatusSize)) nextIconX -= titleStatusSize.width if let image = self.titleRightIconNode.image { - self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width, y: titleFrame.minY + 6.0), size: image.size) + titleTransition.updateFrame(node: self.titleRightIconNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width, y: titleFrame.minY + 6.0), size: image.size)) } } @@ -1148,7 +1148,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { self.superview?.insertSubview(snapshotState.snapshotView, belowSubview: self) let snapshotView = snapshotState.snapshotView - snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -offset.x, y: -offset.y), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift index 8d4bc8fff3..f282b15046 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/FilterSelectorComponent.swift @@ -2,17 +2,19 @@ import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit import PlainButtonComponent -import MultilineTextWithEntitiesComponent +import MultilineTextComponent import BundleIconComponent import TextFormat import AccountContext +import LottieComponent public final class FilterSelectorComponent: Component { public struct Colors: Equatable { public var foreground: UIColor public var background: UIColor - + public init( foreground: UIColor, background: UIColor @@ -24,39 +26,45 @@ public final class FilterSelectorComponent: Component { public struct Item: Equatable { public var id: AnyHashable + public var index: Int public var iconName: String? public var title: String public var action: (UIView) -> Void - + public init( id: AnyHashable, + index: Int = 0, iconName: String? = nil, title: String, action: @escaping (UIView) -> Void ) { self.id = id + self.index = index self.iconName = iconName self.title = title self.action = action } public static func ==(lhs: Item, rhs: Item) -> Bool { - return lhs.id == rhs.id && lhs.iconName == rhs.iconName && lhs.title == rhs.title + return lhs.id == rhs.id && lhs.index == rhs.index && lhs.iconName == rhs.iconName && lhs.title == rhs.title } } - + public let context: AccountContext? public let colors: Colors public let items: [Item] + public let selectedItemId: AnyHashable? public init( context: AccountContext? = nil, colors: Colors, - items: [Item] + items: [Item], + selectedItemId: AnyHashable? ) { self.context = context self.colors = colors self.items = items + self.selectedItemId = selectedItemId } public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool { @@ -69,6 +77,9 @@ public final class FilterSelectorComponent: Component { if lhs.items != rhs.items { return false } + if lhs.selectedItemId != rhs.selectedItemId { + return false + } return true } @@ -123,14 +134,14 @@ public final class FilterSelectorComponent: Component { self.state = state let baseHeight: CGFloat = 28.0 - + var spacing: CGFloat = 6.0 let itemFont = Font.semibold(14.0) let allowScroll = true - + var innerContentWidth: CGFloat = 0.0 - + var validIds: [AnyHashable] = [] var index = 0 var itemViews: [AnyHashable: (VisibleItem, CGSize, ComponentTransition)] = [:] @@ -150,15 +161,17 @@ public final class FilterSelectorComponent: Component { validIds.append(itemId) let itemSize = itemView.title.update( - transition: .immediate, + transition: transition, component: AnyComponent(PlainButtonComponent( content: AnyComponent(ItemComponent( context: component.context, + index: item.index, iconName: item.iconName, text: item.title, font: itemFont, color: component.colors.foreground, - backgroundColor: component.colors.background + backgroundColor: component.colors.background, + isSelected: itemId == component.selectedItemId )), effectAlignment: .center, minSize: nil, @@ -217,7 +230,7 @@ public final class FilterSelectorComponent: Component { self.contentSize = CGSize(width: contentWidth, height: baseHeight) self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width - + return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight) } } @@ -233,43 +246,52 @@ public final class FilterSelectorComponent: Component { extension CGRect { func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect { - return CGRect( + return CGRect( x: self.origin.x * (1.0 - fraction) + (other.origin.x) * fraction, y: self.origin.y * (1.0 - fraction) + (other.origin.y) * fraction, width: self.size.width * (1.0 - fraction) + (other.size.width) * fraction, height: self.size.height * (1.0 - fraction) + (other.size.height) * fraction - ) - } + ) + } } -private final class ItemComponent: CombinedComponent { +private final class ItemComponent: Component { let context: AccountContext? + let index: Int let iconName: String? let text: String let font: UIFont let color: UIColor let backgroundColor: UIColor + let isSelected: Bool init( context: AccountContext?, + index: Int, iconName: String?, text: String, font: UIFont, color: UIColor, - backgroundColor: UIColor + backgroundColor: UIColor, + isSelected: Bool ) { self.context = context + self.index = index self.iconName = iconName self.text = text self.font = font self.color = color self.backgroundColor = backgroundColor + self.isSelected = isSelected } - + static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.index != rhs.index { + return false + } if lhs.iconName != rhs.iconName { return false } @@ -285,44 +307,97 @@ private final class ItemComponent: CombinedComponent { if lhs.backgroundColor != rhs.backgroundColor { return false } + if lhs.isSelected != rhs.isSelected { + return false + } return true } - static var body: Body { - let background = Child(RoundedRectangle.self) - let title = Child(MultilineTextWithEntitiesComponent.self) - let icon = Child(BundleIconComponent.self) + public final class View: UIView { + private var component: ItemComponent? + private weak var state: EmptyComponentState? - return { context in - let component = context.component + private let background = ComponentView() + private let title = ComponentView() + private let icon = ComponentView() + + private var isSelected = false + private var iconName: String? + + private let playOnce = ActionSlot() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state - let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color) - let range = (attributedTitle.string as NSString).range(of: "⭐️") - if range.location != NSNotFound { - attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + var animateTitleInDirection: CGFloat? + if let previousComponent, previousComponent.text != component.text, !transition.animation.isImmediate, let titleView = self.title.view, let snapshotView = titleView.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = titleView.frame + self.addSubview(snapshotView) + + var direction: CGFloat = 1.0 + if previousComponent.index < component.index { + direction = -1.0 + } + + snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 6.0 * direction), duration: 0.2, removeOnCompletion: false, additive: true) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + + animateTitleInDirection = direction } - let title = title.update( - component: MultilineTextWithEntitiesComponent( - context: component.context, - animationCache: component.context?.animationCache, - animationRenderer: component.context?.animationRenderer, - placeholderColor: .white, + let attributedTitle = NSAttributedString(string: component.text, font: component.font, textColor: component.color) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( text: .plain(attributedTitle) - ), - availableSize: context.availableSize, - transition: .immediate + )), + environment: {}, + containerSize: availableSize ) - let icon = icon.update( - component: BundleIconComponent( - name: component.iconName ?? "Item List/ExpandableSelectorArrows", - tintColor: component.color, - maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil - ), - availableSize: CGSize(width: 100, height: 100), - transition: .immediate + let animationName = component.iconName ?? (component.isSelected ? "GiftFilterMenuOpen" : "GiftFilterMenuClose") + let animationSize = component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : CGSize(width: 10.0, height: 22.0) + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: animationName), + color: component.color, + playOnce: self.playOnce + )), + environment: {}, + containerSize: CGSize(width: 22.0, height: 22.0) ) + + var playAnimation = false + if self.isSelected != component.isSelected || self.iconName != component.iconName { + if let iconName = component.iconName { + if component.isSelected { + playAnimation = true + } else if self.iconName != iconName { + playAnimation = true + } + self.iconName = iconName + } else { + playAnimation = true + } + self.isSelected = component.isSelected + } + if playAnimation { + self.playOnce.invoke(Void()) + } let padding: CGFloat = 12.0 var leftPadding = padding @@ -330,35 +405,68 @@ private final class ItemComponent: CombinedComponent { leftPadding -= 4.0 } let spacing: CGFloat = 4.0 - let totalWidth = title.size.width + icon.size.width + spacing + let totalWidth = titleSize.width + animationSize.width + spacing let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0) - let background = background.update( - component: RoundedRectangle( + + let backgroundSize = self.background.update( + transition: transition, + component: AnyComponent(RoundedRectangle( color: component.backgroundColor, cornerRadius: 14.0 - ), - availableSize: size, - transition: .immediate + )), + environment: {}, + containerSize: size ) - context.add(background - .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) - ) - if let _ = component.iconName { - context.add(title - .position(CGPoint(x: size.width - padding - title.size.width / 2.0, y: size.height / 2.0)) - ) - context.add(icon - .position(CGPoint(x: leftPadding + icon.size.width / 2.0, y: size.height / 2.0)) - ) - } else { - context.add(title - .position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0)) - ) - context.add(icon - .position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0)) - ) + + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setPosition(view: backgroundView, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + transition.setBounds(view: backgroundView, bounds: CGRect(origin: CGPoint(), size: backgroundSize)) } + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + let titlePosition: CGPoint + if let _ = component.iconName { + titlePosition = CGPoint(x: size.width - padding - titleSize.width / 2.0, y: size.height / 2.0) + } else { + titlePosition = CGPoint(x: padding + titleSize.width / 2.0, y: size.height / 2.0) + } + if let animateTitleInDirection { + titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + titleView.center = CGPoint(x: titlePosition.x, y: titlePosition.y - 6.0 * animateTitleInDirection) + } + transition.setPosition(view: titleView, position: titlePosition) + titleView.bounds = CGRect(origin: CGPoint(), size: titleSize) + } + + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + let iconPosition: CGPoint + if let _ = component.iconName { + iconPosition = CGPoint(x: leftPadding + iconSize.width / 2.0, y: size.height / 2.0) + } else { + iconPosition = CGPoint(x: size.width - padding - animationSize.width / 2.0, y: size.height / 2.0) + } + transition.setPosition(view: iconView, position: iconPosition) + transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconSize)) + } + return size } } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public 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) + } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift index 28a5e9206a..04f19bba81 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -201,70 +201,27 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu private let actionSelected: (ContextMenuActionResult) -> Void private let scrollNode: ASScrollNode - private let actionNodes: [ContextControllerActionsListActionItemNode] - private let separatorNodes: [ASDisplayNode] + private var actionNodes: [AnyHashable: ContextControllerActionsListActionItemNode] = [:] + private var separatorNodes: [AnyHashable: ASDisplayNode] = [:] private var searchDisposable: Disposable? private var searchQuery = "" + private var itemHeights: [AnyHashable: CGFloat] = [:] + private var totalContentHeight: CGFloat = 0 + private var itemFrames: [AnyHashable: CGRect] = [:] + init(presentationData: PresentationData, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { self.item = item - self.presentationData = presentationData + self.presentationData = presentationData.withUpdate(listsFontSize: .regular) self.getController = getController self.actionSelected = actionSelected self.scrollNode = ASScrollNode() - - var actionNodes: [ContextControllerActionsListActionItemNode] = [] - var separatorNodes: [ASDisplayNode] = [] - - let selectedAttributes = Set(item.selectedAttributes) - - let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { _, f in - getController()?.dismiss(result: .dismissWithoutContent, completion: nil) - - item.selectAll() - }) - - let selectAllActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: selectAllAction) - actionNodes.append(selectAllActionNode) - - let separatorNode = ASDisplayNode() - separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor - separatorNodes.append(separatorNode) - - for attribute in item.attributes { - guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else { - continue - } - let actionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action) - actionNodes.append(actionNode) - if actionNodes.count != item.attributes.count { - let separatorNode = ASDisplayNode() - separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor - separatorNodes.append(separatorNode) - } - } - - let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil - let emptyResultsAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_NoResults, textFont: .small, icon: { _ in return nil }, action: nopAction) - let emptyResultsActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: emptyResultsAction) - actionNodes.append(emptyResultsActionNode) - - self.actionNodes = actionNodes - self.separatorNodes = separatorNodes - + super.init() self.addSubnode(self.scrollNode) - for separatorNode in self.separatorNodes { - self.scrollNode.addSubnode(separatorNode) - } - for actionNode in self.actionNodes { - self.scrollNode.addSubnode(actionNode) - } self.searchDisposable = (item.searchQuery |> deliverOnMainQueue).start(next: { [weak self] searchQuery in @@ -272,15 +229,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu return } self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) - - var i = 1 - for attribute in item.attributes { - guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else { - continue - } - self.actionNodes[i].setItem(item: action) - i += 1 - } + self.invalidateLayout() self.getController()?.requestLayout(transition: .immediate) }) } @@ -297,96 +246,246 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) } - - func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { - let minActionsWidth: CGFloat = 250.0 - let maxActionsWidth: CGFloat = 300.0 - let constrainedWidth = min(constrainedWidth, maxActionsWidth) - var maxWidth: CGFloat = 0.0 - var contentHeight: CGFloat = 0.0 - var heightsAndCompletions: [(Int, CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)] = [] - - + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let maxWidth = self.maxWidth { + self.updateScrolling(maxWidth: maxWidth) + } + } + + enum ItemType { + case selectAll + case attribute(StarGift.UniqueGift.Attribute) + case noResults + case separator + } + + private func getVisibleItems(in scrollView: UIScrollView, constrainedWidth: CGFloat) -> [(itemId: AnyHashable, itemType: ItemType, frame: CGRect)] { let effectiveAttributes: [StarGift.UniqueGift.Attribute] if self.searchQuery.isEmpty { effectiveAttributes = self.item.attributes } else { effectiveAttributes = filteredAttributes(attributes: self.item.attributes, query: self.searchQuery) } - let visibleAttributes = Set(effectiveAttributes.map { attribute -> AnyHashable in - switch attribute { - case let .model(_, file, _): - return file.fileId.id - case let .pattern(_, file, _): - return file.fileId.id - case let .backdrop(_, id, _, _, _, _, _): - return id - default: - fatalError() - } - }) - for i in 0 ..< self.actionNodes.count { - let itemNode = self.actionNodes[i] - if !self.searchQuery.isEmpty && i == 0 { - itemNode.isHidden = true - continue - } + var items: [(itemId: AnyHashable, itemType: ItemType, frame: CGRect)] = [] + var yOffset: CGFloat = 0 + + let defaultHeight: CGFloat = 42.0 + if self.searchQuery.isEmpty { + let selectAllId = AnyHashable("selectAll") + let height = self.itemHeights[selectAllId] ?? defaultHeight + let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height) + items.append((selectAllId, .selectAll, frame)) + yOffset += height - if i > 0 && i < self.actionNodes.count - 1 { - let attribute = self.item.attributes[i - 1] - let attributeId: AnyHashable - switch attribute { - case let .model(_, file, _): - attributeId = AnyHashable(file.fileId.id) - case let .pattern(_, file, _): - attributeId = AnyHashable(file.fileId.id) - case let .backdrop(_, id, _, _, _, _, _): - attributeId = AnyHashable(id) - default: - fatalError() - } - if !visibleAttributes.contains(attributeId) { - itemNode.isHidden = true - continue - } - } - if i == self.actionNodes.count - 1 { - if !visibleAttributes.isEmpty { - itemNode.isHidden = true - continue - } else { - } - } - itemNode.isHidden = false - - let (minSize, complete) = itemNode.update(presentationData: self.presentationData, constrainedSize: CGSize(width: constrainedWidth, height: constrainedHeight)) - maxWidth = max(maxWidth, minSize.width) - heightsAndCompletions.append((i, minSize.height, complete)) - contentHeight += minSize.height + let separatorId = AnyHashable("separator_selectAll") + let separatorFrame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: UIScreenPixel) + items.append((separatorId, .separator, separatorFrame)) + yOffset += UIScreenPixel } - maxWidth = max(maxWidth, minActionsWidth) + for (index, attribute) in effectiveAttributes.enumerated() { + let attributeId = self.getAttributeId(from: attribute) + let height = self.itemHeights[attributeId] ?? defaultHeight + let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height) + items.append((attributeId, .attribute(attribute), frame)) + yOffset += height + + if index < effectiveAttributes.count - 1 { + let separatorId = AnyHashable("separator_\(attributeId)") + let separatorFrame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: UIScreenPixel) + items.append((separatorId, .separator, separatorFrame)) + yOffset += UIScreenPixel + } + } + + if !self.searchQuery.isEmpty && effectiveAttributes.isEmpty { + let noResultsId = AnyHashable("noResults") + let height = self.itemHeights[noResultsId] ?? defaultHeight + let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height) + items.append((noResultsId, .noResults, frame)) + yOffset += height + } + + self.totalContentHeight = yOffset + + for (itemId, _, frame) in items { + self.itemFrames[itemId] = frame + } + + let visibleBounds = scrollView.bounds.insetBy(dx: 0.0, dy: -100.0) + return items.filter { visibleBounds.intersects($0.frame) } + } + + private func getAttributeId(from attribute: StarGift.UniqueGift.Attribute) -> AnyHashable { + switch attribute { + case let .model(_, file, _): + return AnyHashable("model_\(file.fileId.id)") + case let .pattern(_, file, _): + return AnyHashable("pattern_\(file.fileId.id)") + case let .backdrop(_, id, _, _, _, _, _): + return AnyHashable("backdrop_\(id)") + default: + return AnyHashable("unknown") + } + } + + private var maxWidth: CGFloat? + private func updateScrolling(maxWidth: CGFloat) { + let scrollView = self.scrollNode.view + + let constrainedWidth = scrollView.bounds.width + let visibleItems = self.getVisibleItems(in: scrollView, constrainedWidth: constrainedWidth) + + var validNodeIds: Set = [] + + for (itemId, itemType, frame) in visibleItems { + validNodeIds.insert(itemId) + + switch itemType { + case .selectAll: + if self.actionNodes[itemId] == nil { + let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { _, f in + self.getController()?.dismiss(result: .dismissWithoutContent, completion: nil) + self.item.selectAll() + }) + + let actionNode = ContextControllerActionsListActionItemNode( + context: self.item.context, + getController: self.getController, + requestDismiss: self.actionSelected, + requestUpdateAction: { _, _ in }, + item: selectAllAction + ) + self.actionNodes[itemId] = actionNode + self.scrollNode.addSubnode(actionNode) + } + + case .attribute(let attribute): + if self.actionNodes[itemId] == nil { + let selectedAttributes = Set(self.item.selectedAttributes) + guard let action = actionForAttribute( + attribute: attribute, + presentationData: self.presentationData, + selectedAttributes: selectedAttributes, + searchQuery: self.searchQuery, + item: self.item, + getController: self.getController + ) else { continue } + + let actionNode = ContextControllerActionsListActionItemNode( + context: self.item.context, + getController: self.getController, + requestDismiss: self.actionSelected, + requestUpdateAction: { _, _ in }, + item: action + ) + self.actionNodes[itemId] = actionNode + self.scrollNode.addSubnode(actionNode) + } else { + let selectedAttributes = Set(self.item.selectedAttributes) + if let action = actionForAttribute( + attribute: attribute, + presentationData: self.presentationData, + selectedAttributes: selectedAttributes, + searchQuery: self.searchQuery, + item: self.item, + getController: self.getController + ) { + self.actionNodes[itemId]?.setItem(item: action) + } + } + + case .noResults: + if self.actionNodes[itemId] == nil { + let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil + let emptyResultsAction = ContextMenuActionItem( + text: presentationData.strings.Gift_Store_NoResults, + textFont: .small, + icon: { _ in return nil }, + action: nopAction + ) + let actionNode = ContextControllerActionsListActionItemNode( + context: self.item.context, + getController: self.getController, + requestDismiss: self.actionSelected, + requestUpdateAction: { _, _ in }, + item: emptyResultsAction + ) + self.actionNodes[itemId] = actionNode + self.scrollNode.addSubnode(actionNode) + } + case .separator: + if self.separatorNodes[itemId] == nil { + let separatorNode = ASDisplayNode() + separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor + self.separatorNodes[itemId] = separatorNode + self.scrollNode.addSubnode(separatorNode) + } + } + + if let actionNode = self.actionNodes[itemId] { + actionNode.frame = frame + + let (minSize, complete) = actionNode.update(presentationData: self.presentationData, constrainedSize: frame.size) + self.itemHeights[itemId] = minSize.height + complete(CGSize(width: maxWidth, height: minSize.height), .immediate) + } else if let separatorNode = self.separatorNodes[itemId] { + separatorNode.frame = frame + } + } + + var nodesToRemove: [AnyHashable] = [] + for (nodeId, node) in self.actionNodes { + if !validNodeIds.contains(nodeId) { + nodesToRemove.append(nodeId) + node.removeFromSupernode() + } + } + for nodeId in nodesToRemove { + self.actionNodes.removeValue(forKey: nodeId) + } + + var separatorsToRemove: [AnyHashable] = [] + for (separatorId, separatorNode) in self.separatorNodes { + if !validNodeIds.contains(separatorId) { + separatorsToRemove.append(separatorId) + separatorNode.removeFromSupernode() + } + } + for separatorId in separatorsToRemove { + self.separatorNodes.removeValue(forKey: separatorId) + } + } + + private func invalidateLayout() { + self.itemHeights.removeAll() + self.itemFrames.removeAll() + self.totalContentHeight = 0.0 + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let minActionsWidth: CGFloat = 250.0 + let maxActionsWidth: CGFloat = 300.0 + let constrainedWidth = min(constrainedWidth, maxActionsWidth) + let maxWidth = max(constrainedWidth, minActionsWidth) let maxHeight: CGFloat = min(360.0, constrainedHeight - 108.0) - return (CGSize(width: maxWidth, height: min(maxHeight, contentHeight)), { size, transition in - var verticalOffset: CGFloat = 0.0 - for (i, itemHeight, itemCompletion) in heightsAndCompletions { - let itemNode = self.actionNodes[i] - - let itemSize = CGSize(width: maxWidth, height: itemHeight) - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize)) - itemCompletion(itemSize, transition) - verticalOffset += itemHeight - - if i < self.actionNodes.count - 2 { - let separatorNode = self.separatorNodes[i] - separatorNode.frame = CGRect(x: 0, y: verticalOffset, width: size.width, height: UIScreenPixel) - } - } + if self.totalContentHeight == 0 { + let _ = self.getVisibleItems(in: UIScrollView(), constrainedWidth: constrainedWidth) + } + + return (CGSize(width: maxWidth, height: min(maxHeight, self.totalContentHeight)), { size, transition in + self.maxWidth = maxWidth + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) - self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight) + self.scrollNode.view.contentSize = CGSize(width: size.width, height: self.totalContentHeight) + + self.updateScrolling(maxWidth: maxWidth) }) } @@ -417,7 +516,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - for actionNode in self.actionNodes { + for (_, actionNode) in self.actionNodes { actionNode.updateIsHighlighted(isHighlighted: false) } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index c960a11524..1e243f7ba3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -98,6 +98,8 @@ final class GiftStoreScreenComponent: Component { private var initialCount: Int32? private var showLoading = true + private var selectedFilterId: AnyHashable? + private var component: GiftStoreScreenComponent? private(set) weak var state: State? private var environment: EnvironmentType? @@ -502,6 +504,13 @@ final class GiftStoreScreenComponent: Component { }))) let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.selectedFilterId = nil + self.state?.updated() + } controller.presentInGlobalOverlay(contextController) } @@ -603,6 +612,13 @@ final class GiftStoreScreenComponent: Component { items: .single(ContextController.Items(content: .list(items))), gesture: nil ) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.selectedFilterId = nil + self.state?.updated() + } controller.presentInGlobalOverlay(contextController) } @@ -704,6 +720,13 @@ final class GiftStoreScreenComponent: Component { items: .single(ContextController.Items(content: .list(items))), gesture: nil ) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.selectedFilterId = nil + self.state?.updated() + } controller.presentInGlobalOverlay(contextController) } @@ -805,6 +828,13 @@ final class GiftStoreScreenComponent: Component { items: .single(ContextController.Items(content: .list(items))), gesture: nil ) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.selectedFilterId = nil + self.state?.updated() + } controller.presentInGlobalOverlay(contextController) } @@ -996,29 +1026,43 @@ final class GiftStoreScreenComponent: Component { let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 var sortingTitle = environment.strings.Gift_Store_Sort_Date - var sortingIcon: String = "Peer Info/SortDate" + var sortingIcon: String = "GiftFilterDate" + var sortingIndex: Int = 0 if let sorting = self.state?.starGiftsState?.sorting { switch sorting { - case .date: - sortingTitle = environment.strings.Gift_Store_Sort_Date - sortingIcon = "Peer Info/SortDate" case .value: sortingTitle = environment.strings.Gift_Store_Sort_Price - sortingIcon = "Peer Info/SortValue" + sortingIcon = "GiftFilterPrice" + sortingIndex = 0 + case .date: + sortingTitle = environment.strings.Gift_Store_Sort_Date + sortingIcon = "GiftFilterDate" + sortingIndex = 1 case .number: sortingTitle = environment.strings.Gift_Store_Sort_Number - sortingIcon = "Peer Info/SortNumber" + sortingIcon = "GiftFilterNumber" + sortingIndex = 2 } } + enum FilterItemId: Int32 { + case sort + case model + case backdrop + case symbol + } + var filterItems: [FilterSelectorComponent.Item] = [] filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(0), + id: AnyHashable(FilterItemId.sort), + index: sortingIndex, iconName: sortingIcon, title: sortingTitle, action: { [weak self] view in if let self { + self.selectedFilterId = AnyHashable(FilterItemId.sort) self.openSortContextMenu(sourceView: view) + self.state?.updated() } } )) @@ -1035,10 +1079,10 @@ final class GiftStoreScreenComponent: Component { switch attribute { case .model: modelCount += 1 - case .pattern: - symbolCount += 1 case .backdrop: backdropCount += 1 + case .pattern: + symbolCount += 1 } } @@ -1054,29 +1098,35 @@ final class GiftStoreScreenComponent: Component { } filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(1), + id: AnyHashable(FilterItemId.model), title: modelTitle, action: { [weak self] view in if let self { + self.selectedFilterId = AnyHashable(FilterItemId.model) self.openModelContextMenu(sourceView: view) + self.state?.updated() } } )) filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(2), + id: AnyHashable(FilterItemId.backdrop), title: backdropTitle, action: { [weak self] view in if let self { + self.selectedFilterId = AnyHashable(FilterItemId.backdrop) self.openBackdropContextMenu(sourceView: view) + self.state?.updated() } } )) filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(3), + id: AnyHashable(FilterItemId.symbol), title: symbolTitle, action: { [weak self] view in if let self { + self.selectedFilterId = AnyHashable(FilterItemId.symbol) self.openSymbolContextMenu(sourceView: view) + self.state?.updated() } } )) @@ -1092,7 +1142,8 @@ final class GiftStoreScreenComponent: Component { foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65), background: theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85) ), - items: filterItems + items: filterItems, + selectedItemId: self.selectedFilterId )), environment: {}, containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) @@ -1193,8 +1244,17 @@ final class GiftStoreScreenComponent: Component { guard let self else { return } + let previousFilterAttributes = self.starGiftsState?.filterAttributes + let previousSorting = self.starGiftsState?.sorting self.starGiftsState = state - self.updated() + + var transition: ComponentTransition = .immediate + if let previousFilterAttributes, previousFilterAttributes != state.filterAttributes { + transition = .easeInOut(duration: 0.25) + } else if let previousSorting, previousSorting != state.sorting { + transition = .easeInOut(duration: 0.25) + } + self.updated(transition: transition) }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index e01afc1408..b7fb94d32c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1657,6 +1657,10 @@ private final class GiftViewSheetContent: CombinedComponent { convertStars = nil titleString = "" } + + if !canUpgrade, let gift = state.starGiftsMap[giftId], let _ = gift.upgradeStars { + canUpgrade = true + } var showUpgradePreview = false if state.inUpgradePreview, let _ = state.sampleGiftAttributes { diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index 58944fdf9c..70af5bfa7f 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -1890,8 +1890,8 @@ public class TrimView: UIView { effectiveHandleWidth = 16.0 fullTrackHeight = 33.0 capsuleOffset = 8.0 - color = .clear - highlightColor = .clear + color = theme.chat.inputPanel.panelControlAccentColor + highlightColor = theme.chat.inputPanel.panelControlAccentColor self.zoneView.backgroundColor = UIColor(white: 1.0, alpha: 0.4) @@ -1902,7 +1902,19 @@ public class TrimView: UIView { context.fill(CGRect(origin: .zero, size: CGSize(width: 1.0, height: size.height))) context.fill(CGRect(origin: CGPoint(x: size.width - 1.0, y: 0.0), size: CGSize(width: 1.0, height: size.height))) })?.withRenderingMode(.alwaysTemplate).resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 1.0, bottom: 0.0, right: 1.0)) - + + let handleImage = generateImage(CGSize(width: effectiveHandleWidth, height: fullTrackHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height)), cornerRadius: 16.5) + context.addPath(path.cgPath) + context.fillPath() + })?.withRenderingMode(.alwaysTemplate) + + self.leftHandleView.image = handleImage + self.rightHandleView.image = handleImage + self.leftCapsuleView.backgroundColor = .white self.rightCapsuleView.backgroundColor = .white } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 3db8d6c396..52492030e1 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1842,12 +1842,13 @@ public final class MessageInputPanelComponent: Component { } } - let lightFieldColor = UIColor(white: 1.0, alpha: 0.09) + var lightFieldColor = UIColor(white: 1.0, alpha: 0.09) var fieldBackgroundIsDark = false if component.useGrayBackground { fieldBackgroundIsDark = false } else if component.style == .media { fieldBackgroundIsDark = false + lightFieldColor = UIColor(white: 0.2, alpha: 0.45) } else if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText { fieldBackgroundIsDark = true } else if isEditing || component.style == .story || component.style == .editor { diff --git a/submodules/TelegramUI/Resources/Animations/GiftFilterDate.tgs b/submodules/TelegramUI/Resources/Animations/GiftFilterDate.tgs new file mode 100644 index 0000000000..40806c9510 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/GiftFilterDate.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/GiftFilterMenuClose.tgs b/submodules/TelegramUI/Resources/Animations/GiftFilterMenuClose.tgs new file mode 100644 index 0000000000..3616863b50 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/GiftFilterMenuClose.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/GiftFilterMenuOpen.tgs b/submodules/TelegramUI/Resources/Animations/GiftFilterMenuOpen.tgs new file mode 100644 index 0000000000..144e8c46fd Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/GiftFilterMenuOpen.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/GiftFilterNumber.tgs b/submodules/TelegramUI/Resources/Animations/GiftFilterNumber.tgs new file mode 100644 index 0000000000..79c323fac3 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/GiftFilterNumber.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/GiftFilterPrice.tgs b/submodules/TelegramUI/Resources/Animations/GiftFilterPrice.tgs new file mode 100644 index 0000000000..a49ad037a8 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/GiftFilterPrice.tgs differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index ff3a2ebd31..d2d303b741 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -127,7 +127,7 @@ import PostSuggestionsSettingsScreen import ChatSendStarsScreen extension ChatControllerImpl { - func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic, historyNode: ChatHistoryListNodeImpl, isReady: Promise?) { + func reloadChatLocation(chatLocation: ChatLocation, chatLocationContextHolder: Atomic, historyNode: ChatHistoryListNodeImpl, apply: @escaping ((Bool) -> Void) -> Void) { self.contentDataReady.set(false) self.contentDataDisposable?.dispose() @@ -153,35 +153,49 @@ extension ChatControllerImpl { currentChatListFilter: self.currentChatListFilter, customChatNavigationStack: self.customChatNavigationStack, presentationData: self.presentationData, - historyNode: historyNode + historyNode: historyNode, + inviteRequestsContext: self.contentData?.inviteRequestsContext ) self.pendingContentData = (contentData, historyNode) self.contentDataDisposable = (contentData.isReady.get() |> filter { $0 } |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self, weak contentData] _ in + |> deliverOnMainQueue).startStrict(next: { [weak self, weak contentData, weak historyNode] _ in guard let self, let contentData, self.pendingContentData?.contentData === contentData else { return } - self.contentData = contentData - self.pendingContentData = nil - self.contentDataUpdated(synchronous: true, previousState: contentData.state) - self.chatThemeEmoticonPromise.set(contentData.chatThemeEmoticonPromise.get()) - self.chatWallpaperPromise.set(contentData.chatWallpaperPromise.get()) - - self.contentDataReady.set(true) - - contentData.onUpdated = { [weak self, weak contentData] previousState in - guard let self, let contentData, self.contentData === contentData else { + apply({ [weak self, weak contentData] forceAnimation in + guard let self, let contentData, self.pendingContentData?.contentData === contentData else { return } - self.contentDataUpdated(synchronous: false, previousState: previousState) - } + + self.contentData = contentData + self.pendingContentData = nil + self.contentDataUpdated(synchronous: true, forceAnimation: forceAnimation, previousState: contentData.state) + + self.chatThemeEmoticonPromise.set(contentData.chatThemeEmoticonPromise.get()) + self.chatWallpaperPromise.set(contentData.chatWallpaperPromise.get()) + + if let historyNode { + self.setupChatHistoryNode(historyNode: historyNode) + + historyNode.contentPositionChanged(historyNode.visibleContentOffset()) + } + + self.contentDataReady.set(true) + + contentData.onUpdated = { [weak self, weak contentData] previousState in + guard let self, let contentData, self.contentData === contentData else { + return + } + self.contentDataUpdated(synchronous: false, forceAnimation: false, previousState: previousState) + } + }) }) } - func contentDataUpdated(synchronous: Bool, previousState: ContentData.State) { + func contentDataUpdated(synchronous: Bool, forceAnimation: Bool, previousState: ContentData.State) { guard let contentData = self.contentData else { return } @@ -235,7 +249,16 @@ extension ChatControllerImpl { self.chatDisplayNode.overlayTitle = contentData.overlayTitle - self.chatDisplayNode.historyNode.nextChannelToRead = contentData.state.nextChannelToRead + self.chatDisplayNode.historyNode.nextChannelToRead = contentData.state.nextChannelToRead.flatMap { nextChannelToRead -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)? in + return ( + nextChannelToRead.peer, + nextChannelToRead.threadData.flatMap { threadData -> (id: Int64, data: MessageHistoryThreadData) in + return (threadData.id, threadData.data) + }, + nextChannelToRead.unreadCount, + nextChannelToRead.location + ) + } self.chatDisplayNode.historyNode.nextChannelToReadDisplayName = contentData.state.nextChannelToReadDisplayName self.updateNextChannelToReadVisibility() @@ -273,6 +296,18 @@ extension ChatControllerImpl { didDisplayActionsPanel = true } + var previousInvitationPeers: [EnginePeer] = [] + if let requestsState = previousState.requestsState { + previousInvitationPeers = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3)) + } + var previousInvitationRequestsPeersDismissed = false + if let dismissedInvitationRequests = previousState.dismissedInvitationRequests, Set(previousInvitationPeers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) { + previousInvitationRequestsPeersDismissed = true + } + if let requestsState = previousState.requestsState, requestsState.count > 0 && !previousInvitationRequestsPeersDismissed { + didDisplayActionsPanel = true + } + var displayActionsPanel = false if let contactStatus = contentData.state.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { if !peerStatusSettings.flags.isEmpty { @@ -293,19 +328,35 @@ extension ChatControllerImpl { if self.presentationInterfaceState.search != nil && contentData.state.hasSearchTags { displayActionsPanel = true } + + var invitationPeers: [EnginePeer] = [] + if let requestsState = contentData.state.requestsState { + invitationPeers = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3)) + } + var invitationRequestsPeersDismissed = false + if let dismissedInvitationRequests = contentData.state.dismissedInvitationRequests, Set(invitationPeers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) { + invitationRequestsPeersDismissed = true + } + if let requestsState = contentData.state.requestsState, requestsState.count > 0 && !invitationRequestsPeersDismissed { + displayActionsPanel = true + } + if displayActionsPanel != didDisplayActionsPanel { animated = true } - if previousState.pinnedMessage != contentData.state.pinnedMessage { animated = true } + if forceAnimation { + animated = true + } self.updateChatPresentationInterfaceState(animated: animated && self.willAppear, interactive: false, { presentationInterfaceState in var presentationInterfaceState = presentationInterfaceState presentationInterfaceState = presentationInterfaceState.updatedPeer({ _ in return contentData.state.renderedPeer }) + presentationInterfaceState = presentationInterfaceState.updatedChatLocation(contentData.chatLocation) presentationInterfaceState = presentationInterfaceState.updatedIsNotAccessible(contentData.state.isNotAccessible) presentationInterfaceState = presentationInterfaceState.updatedContactStatus(contentData.state.contactStatus) presentationInterfaceState = presentationInterfaceState.updatedHasBots(contentData.state.hasBots) @@ -394,7 +445,6 @@ extension ChatControllerImpl { if let dismissedInvitationRequests = contentData.state.dismissedInvitationRequests, Set(peers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) { peersDismissed = true } - if let requestsState = contentData.state.requestsState, requestsState.count > 0 && !peersDismissed { if !context.contains(where: { switch $0 { @@ -749,7 +799,7 @@ extension ChatControllerImpl { })).startStandalone() } - self.setupChatHistoryNode() + self.setupChatHistoryNode(historyNode: self.chatDisplayNode.historyNode) self.chatDisplayNode.requestLayout = { [weak self] transition in self?.requestLayout(transition: transition) @@ -1206,7 +1256,7 @@ extension ChatControllerImpl { self.scrollToEndOfHistory() } } else { - if let messageId = self.historyNavigationStack.removeLast() { + if let messageId = self.contentData?.historyNavigationStack.removeLast() { self.navigateToMessage(from: nil, to: .id(messageId.id, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false) } else { if case .known = self.chatDisplayNode.historyNode.visibleContentOffset() { @@ -4295,7 +4345,7 @@ extension ChatControllerImpl { self.displayNodeDidLoad() } - func setupChatHistoryNode() { + func setupChatHistoryNode(historyNode: ChatHistoryListNodeImpl) { do { let peerId = self.chatLocation.peerId if let subject = self.subject, case .scheduledMessages = subject { @@ -4582,8 +4632,10 @@ extension ChatControllerImpl { } } - self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in - guard let strongSelf = self else { return } + historyNode.contentPositionChanged = { [weak self, weak historyNode] offset in + guard let strongSelf = self, let historyNode, strongSelf.chatDisplayNode.historyNode === historyNode else { + return + } var minOffsetForNavigation: CGFloat = 40.0 strongSelf.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in @@ -4632,7 +4684,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut)) } - self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toSubject, initial in + historyNode.scrolledToIndex = { [weak self] toSubject, initial in if let strongSelf = self, case let .message(index) = toSubject.index { if case let .message(messageSubject, _, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id { if messageId.peerId == index.id.peerId { @@ -4693,16 +4745,16 @@ extension ChatControllerImpl { } } - self.chatDisplayNode.historyNode.scrolledToSomeIndex = { [weak self] in + historyNode.scrolledToSomeIndex = { [weak self] in guard let strongSelf = self else { return } strongSelf.contentData?.scrolledToMessageIdValue = nil } - self.chatDisplayNode.historyNode.maxVisibleMessageIndexUpdated = { [weak self] index in - if let strongSelf = self, !strongSelf.historyNavigationStack.isEmpty { - strongSelf.historyNavigationStack.filterOutIndicesLessThan(index) + historyNode.maxVisibleMessageIndexUpdated = { [weak self] index in + if let strongSelf = self, let contentData = strongSelf.contentData, !contentData.historyNavigationStack.isEmpty { + contentData.historyNavigationStack.filterOutIndicesLessThan(index) } } @@ -4723,7 +4775,7 @@ extension ChatControllerImpl { hasActiveCalls = .single(false) } - let shouldBeActive = combineLatest(self.context.sharedContext.mediaManager.audioSession.isPlaybackActive() |> deliverOnMainQueue, self.chatDisplayNode.historyNode.hasVisiblePlayableItemNodes, hasActiveCalls) + let shouldBeActive = combineLatest(self.context.sharedContext.mediaManager.audioSession.isPlaybackActive() |> deliverOnMainQueue, historyNode.hasVisiblePlayableItemNodes, hasActiveCalls) |> mapToSignal { [weak self] isPlaybackActive, hasVisiblePlayableItemNodes, hasActiveCalls -> Signal in if hasVisiblePlayableItemNodes && !isPlaybackActive && !hasActiveCalls { return Signal { [weak self] subscriber in @@ -4776,7 +4828,7 @@ extension ChatControllerImpl { downPressed: buttonAction ) - self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer, threadData, location in + historyNode.openNextChannelToRead = { [weak self] peer, threadData, location in guard let strongSelf = self else { return } @@ -4831,7 +4883,7 @@ extension ChatControllerImpl { } } - self.chatDisplayNode.historyNode.beganDragging = { [weak self] in + historyNode.beganDragging = { [weak self] in guard let self else { return } @@ -4846,7 +4898,7 @@ extension ChatControllerImpl { } } - self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, transition, itemNode, isTracking in + historyNode.didScrollWithOffset = { [weak self] offset, transition, itemNode, isTracking in guard let strongSelf = self else { return } @@ -4876,23 +4928,22 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.loadingPlaceholderNode?.addContentOffset(offset: offset, transition: transition) } strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode, isRotated: strongSelf.chatDisplayNode.historyNode.rotated) - } - self.chatDisplayNode.historyNode.hasAtLeast3MessagesUpdated = { [weak self] hasAtLeast3Messages in + historyNode.hasAtLeast3MessagesUpdated = { [weak self] hasAtLeast3Messages in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasAtLeast3Messages(hasAtLeast3Messages) }) } } - self.chatDisplayNode.historyNode.hasPlentyOfMessagesUpdated = { [weak self] hasPlentyOfMessages in + historyNode.hasPlentyOfMessagesUpdated = { [weak self] hasPlentyOfMessages in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) }) } } if self.didAppear { - self.chatDisplayNode.historyNode.canReadHistory.set(self.computedCanReadHistoryPromise.get()) + historyNode.canReadHistory.set(self.computedCanReadHistoryPromise.get()) } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 832c7fe53f..9b11b9cd17 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -317,7 +317,7 @@ extension ChatControllerImpl { }) } else if forceInCurrentChat { if let _ = fromId, let fromIndex = fromIndex, rememberInStack { - self.historyNavigationStack.add(fromIndex) + self.contentData?.historyNavigationStack.add(fromIndex) } let scrollFromIndex: MessageIndex? @@ -505,7 +505,7 @@ extension ChatControllerImpl { return } if let _ = fromId, rememberInStack { - self.historyNavigationStack.add(fromIndex) + self.contentData?.historyNavigationStack.add(fromIndex) } self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 5f4dae9e2a..15239ef82f 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -23,8 +23,6 @@ func updateChatPresentationInterfaceStateImpl( _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void ) { - let previousChatLocation = selfController.chatDisplayNode.historyNode.chatLocation - var completion = externalCompletion var temporaryChatPresentationInterfaceState = f(selfController.presentationInterfaceState) @@ -436,14 +434,6 @@ func updateChatPresentationInterfaceStateImpl( selfController.presentationInterfaceState = updatedChatPresentationInterfaceState - if selfController.chatDisplayNode.chatLocation != selfController.presentationInterfaceState.chatLocation { - let defaultDirection: ChatControllerAnimateInnerChatSwitchDirection? = selfController.chatDisplayNode.chatLocationTabSwitchDirection(from: selfController.chatLocation.threadId, to: selfController.presentationInterfaceState.chatLocation.threadId).flatMap { direction -> ChatControllerAnimateInnerChatSwitchDirection in - return direction ? .right : .left - } - let tabSwitchDirection = selfController.currentChatSwitchDirection ?? defaultDirection - selfController.chatDisplayNode.updateChatLocation(chatLocation: selfController.presentationInterfaceState.chatLocation, transition: transition, tabSwitchDirection: tabSwitchDirection) - } - selfController.updateSlowmodeStatus() switch updatedChatPresentationInterfaceState.inputMode { @@ -500,20 +490,11 @@ func updateChatPresentationInterfaceStateImpl( } var buttonsAnimated = transition.isAnimated - if selfController.currentChatSwitchDirection != nil { - buttonsAnimated = false - } if let button = rightNavigationButtonForChatInterfaceState(context: selfController.context, presentationInterfaceState: updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.rightNavigationButton, target: selfController, selector: #selector(selfController.rightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { if selfController.rightNavigationButton != button { if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { buttonsAnimated = false } - if case .replyThread = selfController.chatLocation { - buttonsAnimated = false - } - if let channel = updatedChatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { - buttonsAnimated = false - } selfController.rightNavigationButton = button } } else if let _ = selfController.rightNavigationButton { @@ -603,12 +584,6 @@ func updateChatPresentationInterfaceStateImpl( } } - if previousChatLocation != selfController.presentationInterfaceState.chatLocation { - selfController.chatLocation = selfController.presentationInterfaceState.chatLocation - selfController.reloadCachedData() - selfController.setupChatHistoryNode() - } - selfController.updateDownButtonVisibility() if selfController.presentationInterfaceState.hasBirthdayToday { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8dd7ae9ec8..ad2de43f0f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -389,8 +389,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var searchDisposable: MetaDisposable? - var historyNavigationStack = ChatHistoryNavigationStack() - public let canReadHistory = ValuePromise(true, ignoreRepeated: true) public let hasBrowserOrAppInFront = Promise(false) @@ -5230,7 +5228,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - self.reloadChatLocation(chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, historyNode: self.chatDisplayNode.historyNode, isReady: nil) + self.reloadChatLocation(chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, historyNode: self.chatDisplayNode.historyNode, apply: { $0(false) }) self.botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get() |> deliverOnMainQueue).startStrict(next: { [weak self] message in @@ -9711,10 +9709,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var currentChatSwitchDirection: ChatControllerAnimateInnerChatSwitchDirection? public func updateChatLocationThread(threadId: Int64?, animationDirection: ChatControllerAnimateInnerChatSwitchDirection? = nil) { - /*if self.isUpdatingChatLocationThread { + if self.isUpdatingChatLocationThread { return } - guard let peerId = self.chatLocation.peerId else { return } @@ -9725,16 +9722,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let navigationSnapshot = self.chatTitleView?.prepareSnapshotState() - - //let historyNode = self.chatDisplayNode.createHistoryNodeForChatLocation(chatLocation: self.chatLocation, chatLocationContextHolder: ) - - let rightBarButtonItemSnapshots: [(UIView, CGRect)] = (self.navigationItem.rightBarButtonItems ?? []).compactMap { item -> (UIView, CGRect)? in - guard let view = item.customDisplayNode?.view, let snapshotView = view.snapshotView(afterScreenUpdates: false) else { - return nil - } - return (snapshotView, view.convert(view.bounds, to: self.view)) - } + self.saveInterfaceState() let updatedChatLocation: ChatLocation if let threadId { @@ -9762,25 +9750,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G updatedChatLocation = .peer(id: peerId) } - let isReady = Promise() - let chatLocationContextHolder = Atomic(value: nil) - self.reloadChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder, historyNode: historyNode, isReady: isReady) + let navigationSnapshot = self.chatTitleView?.prepareSnapshotState() + let avatarSnapshot = self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil ? (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState() : nil + let chatLocationContextHolder = Atomic(value: nil) + let historyNode = self.chatDisplayNode.createHistoryNodeForChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder) self.isUpdatingChatLocationThread = true - self.updateChatLocationThreadDisposable?.dispose() - self.updateChatLocationThreadDisposable = (isReady.get() - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] _ in - guard let self else { + self.reloadChatLocation(chatLocation: updatedChatLocation, chatLocationContextHolder: chatLocationContextHolder, historyNode: historyNode, apply: { [weak self, weak historyNode] apply in + guard let self, let historyNode else { return } - self.isUpdatingChatLocationThread = false self.currentChatSwitchDirection = animationDirection - self.updateChatPresentationInterfaceState(animated: animationDirection != nil, interactive: false, { presentationInterfaceState in - return presentationInterfaceState.updatedChatLocation(updatedChatLocation) - }) + self.chatLocation = updatedChatLocation + self.chatDisplayNode.prepareSwitchToChatLocation(historyNode: historyNode, animationDirection: animationDirection) + + apply(true) if let navigationSnapshot, let animationDirection { let mappedAnimationDirection: ChatTitleView.AnimateFromSnapshotDirection @@ -9796,33 +9781,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.chatTitleView?.animateFromSnapshot(navigationSnapshot, direction: mappedAnimationDirection) - if let rightBarButtonItems = self.navigationItem.rightBarButtonItems { - for i in 0 ..< rightBarButtonItems.count { - let item = rightBarButtonItems[i] - if let customDisplayNode = item.customDisplayNode { - customDisplayNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - //customDisplayNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - - let _ = rightBarButtonItemSnapshots - /*if i < rightBarButtonItemSnapshots.count { - let (snapshotItem, snapshotFrame) = rightBarButtonItemSnapshots[i] - if let targetSuperview = customDisplayNode.view.superview { - snapshotItem.frame = targetSuperview.convert(snapshotFrame, from: self.view) - targetSuperview.addSubview(snapshotItem) - - snapshotItem.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, completion: { [weak snapshotItem] _ in - snapshotItem?.removeFromSuperview() - }) - snapshotItem.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - } - }*/ - } - } - } + } + + if let avatarSnapshot, self.chatInfoNavigationButton?.buttonItem.customDisplayNode?.view.window != nil { + (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshot) } self.currentChatSwitchDirection = nil - })*/ + self.isUpdatingChatLocationThread = false + }) } public var contentContainerNode: ASDisplayNode { diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index 48046d104a..275dafc0a9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerContentData.swift +++ b/submodules/TelegramUI/Sources/ChatControllerContentData.swift @@ -57,6 +57,30 @@ extension ChatControllerImpl { case dismiss } + struct NextChannelToRead: Equatable { + struct ThreadData: Equatable { + let id: Int64 + let data: MessageHistoryThreadData + + init(id: Int64, data: MessageHistoryThreadData) { + self.id = id + self.data = data + } + } + + let peer: EnginePeer + let threadData: ThreadData? + let unreadCount: Int + let location: TelegramEngine.NextUnreadChannelLocation + + init(peer: EnginePeer, threadData: ThreadData?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) { + self.peer = peer + self.threadData = threadData + self.unreadCount = unreadCount + self.location = location + } + } + struct State { var peerView: PeerView? var threadInfo: EngineMessageHistoryThread.Info? @@ -72,7 +96,7 @@ extension ChatControllerImpl { var contactStatus: ChatContactStatus? var adMessage: Message? var offerNextChannelToRead: Bool = false - var nextChannelToRead: (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)? + var nextChannelToRead: NextChannelToRead? var nextChannelToReadDisplayName: Bool = false var isNotAccessible: Bool = false var hasBots: Bool = false @@ -144,6 +168,7 @@ extension ChatControllerImpl { private let isChatLocationInfoReady = ValuePromise(false, ignoreRepeated: true) private let isCachedDataReady = ValuePromise(false, ignoreRepeated: true) + let chatLocation: ChatLocation let chatLocationInfoData: ChatLocationInfoData private(set) var state: State = State() @@ -172,11 +197,13 @@ extension ChatControllerImpl { } } + var historyNavigationStack = ChatHistoryNavigationStack() + let chatThemeEmoticonPromise = Promise() let chatWallpaperPromise = Promise() - var inviteRequestsContext: PeerInvitationImportersContext? - private var inviteRequestsDisposable = MetaDisposable() + private(set) var inviteRequestsContext: PeerInvitationImportersContext? + private var inviteRequestsDisposable: Disposable? init( context: AccountContext, @@ -189,9 +216,14 @@ extension ChatControllerImpl { currentChatListFilter: Int32?, customChatNavigationStack: [EnginePeer.Id]?, presentationData: PresentationData, - historyNode: ChatHistoryListNodeImpl + historyNode: ChatHistoryListNodeImpl, + inviteRequestsContext: PeerInvitationImportersContext? ) { + self.chatLocation = chatLocation self.presentationData = presentationData + + self.inviteRequestsContext = inviteRequestsContext + let strings = self.presentationData.strings let chatLocationPeerId: PeerId? = chatLocation.peerId @@ -968,11 +1000,23 @@ extension ChatControllerImpl { let previousState = strongSelf.state - strongSelf.state.offerNextChannelToRead = true - strongSelf.state.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer, threadData: nil, unreadCount: 0, location: .same) + var isUpdated = false + + if !strongSelf.state.offerNextChannelToRead { + strongSelf.state.offerNextChannelToRead = true + isUpdated = true + } + let nextChannelToRead = nextPeer.flatMap { nextPeer -> NextChannelToRead in + return NextChannelToRead(peer: nextPeer, threadData: nil, unreadCount: 0, location: .same) + } + if strongSelf.state.nextChannelToRead != nextChannelToRead { + strongSelf.state.nextChannelToRead = nextChannelToRead + isUpdated = true + } + if strongSelf.state.nextChannelToReadDisplayName != (nextChatSuggestionTip >= 3) { + strongSelf.state.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + isUpdated = true } - strongSelf.state.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 let nextPeerId = nextPeer?.id @@ -988,7 +1032,9 @@ extension ChatControllerImpl { } } - strongSelf.onUpdated?(previousState) + if isUpdated { + strongSelf.onUpdated?(previousState) + } }) } } else if isRegularChat, strongSelf.nextChannelToReadDisposable == nil { @@ -1008,11 +1054,23 @@ extension ChatControllerImpl { let previousState = strongSelf.state - strongSelf.state.offerNextChannelToRead = true - strongSelf.state.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer.peer, threadData: nil, unreadCount: nextPeer.unreadCount, location: nextPeer.location) + var isUpdated = false + + if !strongSelf.state.offerNextChannelToRead { + strongSelf.state.offerNextChannelToRead = true + isUpdated = true + } + let nextChannelToRead = nextPeer.flatMap { nextPeer -> NextChannelToRead in + return NextChannelToRead(peer: nextPeer.peer, threadData: nil, unreadCount: nextPeer.unreadCount, location: nextPeer.location) + } + if strongSelf.state.nextChannelToRead != nextChannelToRead { + strongSelf.state.nextChannelToRead = nextChannelToRead + isUpdated = true + } + if strongSelf.state.nextChannelToReadDisplayName != (nextChatSuggestionTip >= 3) { + strongSelf.state.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + isUpdated = true } - strongSelf.state.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 let nextPeerId = nextPeer?.peer.id @@ -1028,7 +1086,9 @@ extension ChatControllerImpl { } } - strongSelf.onUpdated?(previousState) + if isUpdated { + strongSelf.onUpdated?(previousState) + } }) } } @@ -1566,13 +1626,27 @@ extension ChatControllerImpl { let previousState = strongSelf.state - strongSelf.state.offerNextChannelToRead = true - strongSelf.state.nextChannelToRead = nextThreadData.flatMap { nextThreadData -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: EnginePeer(channel), threadData: nextThreadData, unreadCount: Int(nextThreadData.data.incomingUnreadCount), location: .same) - } - strongSelf.state.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + var isUpdated = false - strongSelf.onUpdated?(previousState) + if !strongSelf.state.offerNextChannelToRead { + strongSelf.state.offerNextChannelToRead = true + isUpdated = true + } + let nextChannelToRead = nextThreadData.flatMap { nextThreadData -> NextChannelToRead in + return NextChannelToRead(peer: EnginePeer(channel), threadData: NextChannelToRead.ThreadData(id: nextThreadData.id, data: nextThreadData.data), unreadCount: Int(nextThreadData.data.incomingUnreadCount), location: .same) + } + if strongSelf.state.nextChannelToRead != nextChannelToRead { + strongSelf.state.nextChannelToRead = nextChannelToRead + isUpdated = true + } + if strongSelf.state.nextChannelToReadDisplayName != (nextChatSuggestionTip >= 3) { + strongSelf.state.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + isUpdated = true + } + + if isUpdated { + strongSelf.onUpdated?(previousState) + } }) } } @@ -2125,19 +2199,6 @@ extension ChatControllerImpl { if strongSelf.inviteRequestsContext == nil { let inviteRequestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil)) strongSelf.inviteRequestsContext = inviteRequestsContext - - strongSelf.inviteRequestsDisposable.set((combineLatest(queue: Queue.mainQueue(), inviteRequestsContext.state, ApplicationSpecificNotice.dismissedInvitationRequests(accountManager: context.sharedContext.accountManager, peerId: peerId))).startStrict(next: { [weak strongSelf] requestsState, dismissedInvitationRequests in - guard let strongSelf else { - return - } - - let previousState = strongSelf.state - - strongSelf.state.requestsState = requestsState - strongSelf.state.dismissedInvitationRequests = dismissedInvitationRequests - - strongSelf.onUpdated?(previousState) - })) } else if let inviteRequestsContext = strongSelf.inviteRequestsContext { let _ = (inviteRequestsContext.state |> take(1) @@ -2147,6 +2208,30 @@ extension ChatControllerImpl { } }) } + + if chatLocation.threadId == nil { + if strongSelf.inviteRequestsDisposable == nil, let inviteRequestsContext = strongSelf.inviteRequestsContext { + strongSelf.inviteRequestsDisposable = combineLatest(queue: Queue.mainQueue(), inviteRequestsContext.state, ApplicationSpecificNotice.dismissedInvitationRequests(accountManager: context.sharedContext.accountManager, peerId: peerId)).startStrict(next: { [weak strongSelf] requestsState, dismissedInvitationRequests in + guard let strongSelf else { + return + } + + let previousState = strongSelf.state + + strongSelf.state.requestsState = requestsState + strongSelf.state.dismissedInvitationRequests = dismissedInvitationRequests + + strongSelf.onUpdated?(previousState) + }) + } + } else { + strongSelf.state.requestsState = nil + strongSelf.state.dismissedInvitationRequests = [] + } + } else { + strongSelf.inviteRequestsContext = nil + strongSelf.state.requestsState = nil + strongSelf.state.dismissedInvitationRequests = [] } var isUpdated = false @@ -2192,7 +2277,7 @@ extension ChatControllerImpl { self.cachedDataDisposable?.dispose() self.premiumGiftSuggestionDisposable?.dispose() self.translationStateDisposable?.dispose() - self.inviteRequestsDisposable.dispose() + self.inviteRequestsDisposable?.dispose() } } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 06f9eaef63..62bc7a610a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -133,6 +133,19 @@ class HistoryNodeContainer: ASDisplayNode { } } +private final class PendingSwitchToChatLocation { + let historyNode: ChatHistoryListNodeImpl + let animationDirection: ChatControllerAnimateInnerChatSwitchDirection? + + init( + historyNode: ChatHistoryListNodeImpl, + animationDirection: ChatControllerAnimateInnerChatSwitchDirection? + ) { + self.historyNode = historyNode + self.animationDirection = animationDirection + } +} + class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let context: AccountContext private(set) var chatLocation: ChatLocation @@ -162,7 +175,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var historyNode: ChatHistoryListNodeImpl var blurredHistoryNode: ASImageNode? let historyNodeContainer: HistoryNodeContainer - let loadingNode: ChatLoadingNode + private(set) var loadingNode: ChatLoadingNode + + private var isLoadingValue: Bool = false + private var isLoadingEarlier: Bool = false private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? var alwaysShowSearchResultsAsList: Bool = false @@ -316,8 +332,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let adMessagesContext: AdMessagesHistoryContext? - private var isLoadingValue: Bool = false - private var isLoadingEarlier: Bool = false + private var pendingSwitchToChatLocation: PendingSwitchToChatLocation? + private func updateIsLoading(isLoading: Bool, earlier: Bool, animated: Bool) { var useLoadingPlaceholder = self.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser && self.chatLocation.peerId?.namespace != Namespaces.Peer.SecretChat if case let .replyThread(message) = self.chatLocation, message.peerId == self.context.account.peerId { @@ -351,10 +367,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { loadingPlaceholderNode.setup(self.historyNode, updating: false) - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate, listViewTransaction: { _, _, _, _ in - }, updateExtraNavigationBarBackgroundHeight: { _, _, _, _ in - }) + let contentBounds = self.loadingNode.frame + loadingPlaceholderNode.frame = contentBounds + if let loadingPlaceholderNode = self.loadingPlaceholderNode, let validLayout = self.validLayout { + loadingPlaceholderNode.updateLayout(size: contentBounds.size, insets: self.visibleAreaInset, metrics: validLayout.0.metrics, transition: .immediate) + loadingPlaceholderNode.update(rect: contentBounds, within: contentBounds.size, transition: .immediate) } } loadingPlaceholderNode.alpha = 1.0 @@ -371,12 +388,17 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } else { if useLoadingPlaceholder { if let loadingPlaceholderNode = self.loadingPlaceholderNode { - loadingPlaceholderNode.animateOut(self.historyNode, completion: { [weak self] in - if let strongSelf = self { - strongSelf.loadingPlaceholderNode?.removeFromSupernode() - strongSelf.loadingPlaceholderNode = nil - } - }) + if animated { + loadingPlaceholderNode.animateOut(self.historyNode, completion: { [weak self] in + if let strongSelf = self { + strongSelf.loadingPlaceholderNode?.removeFromSupernode() + strongSelf.loadingPlaceholderNode = nil + } + }) + } else { + self.loadingPlaceholderNode = nil + loadingPlaceholderNode.removeFromSupernode() + } } } else { self.loadingNode.alpha = 0.0 @@ -1295,8 +1317,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) - var extraTransition = transition - var dismissedTitleTopicsAccessoryPanelNode: ChatTopicListTitleAccessoryPanelNode? var immediatelyLayoutTitleTopicsAccessoryPanelNodeAndAnimateAppearance = false var titleTopicsAccessoryPanelHeight: CGFloat? @@ -1357,9 +1377,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.titleAccessoryPanelContainer.addSubnode(titleAccessoryPanelNode) titleAccessoryPanelNode.clipsToBounds = true - if transition.isAnimated { - extraTransition = .animated(duration: 0.2, curve: .easeInOut) - } } let layoutResult = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) @@ -1367,9 +1384,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { titleAccessoryPanelBackgroundHeight = layoutResult.backgroundHeight titleAccessoryPanelHitTestSlop = layoutResult.hitTestSlop if immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance { - titleAccessoryPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if transition.isAnimated { + titleAccessoryPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } titleAccessoryPanelNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -layoutResult.backgroundHeight, 0.0) - extraTransition.updateSublayerTransformOffset(layer: titleAccessoryPanelNode.layer, offset: CGPoint()) + transition.updateSublayerTransformOffset(layer: titleAccessoryPanelNode.layer, offset: CGPoint()) } } else if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = titleAccessoryPanelNode @@ -1412,9 +1431,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.titleAccessoryPanelContainer.addSubnode(translationPanelNode) translationPanelNode.clipsToBounds = true - if transition.isAnimated { - extraTransition = .animated(duration: 0.2, curve: .easeInOut) - } } let height = translationPanelNode.updateLayout(width: layout.size.width, leftInset: leftPanelSize?.width ?? layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) @@ -1422,7 +1438,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if immediatelyLayoutTranslationPanelNodeAndAnimateAppearance { translationPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) translationPanelNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -height, 0.0) - extraTransition.updateSublayerTransformOffset(layer: translationPanelNode.layer, offset: CGPoint()) + transition.updateSublayerTransformOffset(layer: translationPanelNode.layer, offset: CGPoint()) } } else if let chatTranslationPanel = self.chatTranslationPanel { dismissedTranslationPanelNode = chatTranslationPanel @@ -1803,18 +1819,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { wrappingInsets.top += statusBarHeight } } - - var isSelectionEnabled = true - if previewing { - isSelectionEnabled = false - } else if case .pinnedMessages = self.chatPresentationInterfaceState.subject { - isSelectionEnabled = false - } else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { - isSelectionEnabled = false - } else if case .customChatContents = self.chatLocation { - isSelectionEnabled = false - } - self.historyNode.isSelectionGestureEnabled = isSelectionEnabled transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 200.0))) self.titleAccessoryPanelContainer.hitTestExcludeInsets = UIEdgeInsets(top: 0.0, left: leftPanelSize?.width ?? 0.0, bottom: 0.0, right: 0.0) @@ -1839,6 +1843,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } var titleAccessoryPanelFrame: CGRect? + let titleAccessoryPanelBaseY = titlePanelsContentOffset if let _ = self.titleAccessoryPanelNode, let panelHeight = titleAccessoryPanelHeight { titleAccessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: titlePanelsContentOffset), size: CGSize(width: layout.size.width, height: panelHeight)) insets.top += panelHeight @@ -1881,7 +1886,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { extraNavigationBarLeftCutout = CGSize(width: 0.0, height: navigationBarHeight) } - updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraNavigationBarLeftCutout, extraTransition) + updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraNavigationBarLeftCutout, transition) let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) @@ -1908,13 +1913,154 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateBounds(node: self.historyNodeContainer, bounds: contentBounds) transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center) - transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) - transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) + if let pendingSwitchToChatLocation = self.pendingSwitchToChatLocation { + self.pendingSwitchToChatLocation = nil + + let previousHistoryNode = self.historyNode + self.historyNode = pendingSwitchToChatLocation.historyNode + + self.historyNode.position = previousHistoryNode.position + self.historyNode.bounds = previousHistoryNode.bounds + self.historyNode.transform = previousHistoryNode.transform + + self.historyNode.messageTransitionNode = { [weak self] in + guard let self else { + return nil + } + return self.messageTransitionNode + } + + transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) + transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) + + previousHistoryNode.supernode?.insertSubnode(self.historyNode, aboveSubnode: previousHistoryNode) + + let messageTransitionNode = ChatMessageTransitionNodeImpl(listNode: self.historyNode, getContentAreaInScreenSpace: { [weak self] in + guard let self else { + return CGRect() + } + return self.view.convert(self.frameForVisibleArea(), to: nil) + }, onTransitionEvent: { [weak self] transition in + guard let self else { + return + } + if (self.context.sharedContext.currentPresentationData.with({ $0 })).reduceMotion { + return + } + if self.context.sharedContext.energyUsageSettings.fullTranslucency { + self.backgroundNode.animateEvent(transition: transition, extendAnimation: false) + } + }) + + let previousMessageTransitionNode = self.messageTransitionNode + self.messageTransitionNode = messageTransitionNode + + messageTransitionNode.position = previousMessageTransitionNode.position + messageTransitionNode.bounds = previousMessageTransitionNode.bounds + messageTransitionNode.transform = previousMessageTransitionNode.transform + + self.wrappingNode.contentNode.insertSubnode(self.messageTransitionNode, aboveSubnode: previousMessageTransitionNode) + + self.emptyType = nil + self.isLoadingValue = false + self.isLoadingEarlier = false + + let previousLoadingNode = self.loadingNode + self.loadingNode = ChatLoadingNode(context: self.context, theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners) + self.loadingNode.frame = previousLoadingNode.frame + self.loadingNode.isHidden = previousLoadingNode.isHidden + self.loadingNode.alpha = previousLoadingNode.alpha + previousLoadingNode.supernode?.insertSubnode(self.loadingNode, aboveSubnode: previousLoadingNode) + + let previousLoadingPlaceholderNode = self.loadingPlaceholderNode + self.loadingPlaceholderNode = nil + + let previousEmptyNode = self.emptyNode + self.emptyNode = nil + + self.setupHistoryNode() + self.historyNode.loadStateUpdated?(self.historyNode.loadState ?? .messages, false) + + if let animationDirection = pendingSwitchToChatLocation.animationDirection { + var offsetMultiplier = CGPoint() + switch animationDirection { + case .up: + offsetMultiplier.y = -1.0 + case .down: + offsetMultiplier.y = 1.0 + case .left: + offsetMultiplier.x = -1.0 + case .right: + offsetMultiplier.x = 1.0 + } + + previousHistoryNode.clipsToBounds = true + self.historyNode.clipsToBounds = true + + transition.animatePosition(layer: self.historyNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true, completion: { [weak self] _ in + guard let self else { + return + } + self.historyNode.clipsToBounds = false + }) + transition.animatePosition(layer: previousHistoryNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier.x * layout.size.width, y: -offsetMultiplier.y * layout.size.height), removeOnCompletion: false, additive: true, completion: { [weak previousHistoryNode] _ in + previousHistoryNode?.removeFromSupernode() + }) + + transition.animatePosition(layer: self.messageTransitionNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true) + transition.animatePosition(layer: previousMessageTransitionNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier.x * layout.size.width, y: -offsetMultiplier.y * layout.size.height), removeOnCompletion: false, additive: true, completion: { [weak previousMessageTransitionNode] _ in + previousMessageTransitionNode?.removeFromSupernode() + }) + + transition.animatePosition(layer: self.loadingNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true) + transition.animatePosition(layer: previousLoadingNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier.x * layout.size.width, y: -offsetMultiplier.y * layout.size.height), removeOnCompletion: false, additive: true, completion: { [weak previousLoadingNode] _ in + previousLoadingNode?.removeFromSupernode() + }) + + if let loadingPlaceholderNode = self.loadingPlaceholderNode { + transition.animatePosition(layer: loadingPlaceholderNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true) + } + if let previousLoadingPlaceholderNode { + transition.animatePosition(layer: previousLoadingPlaceholderNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier.x * layout.size.width, y: -offsetMultiplier.y * layout.size.height), removeOnCompletion: false, additive: true, completion: { [weak previousLoadingPlaceholderNode] _ in + previousLoadingPlaceholderNode?.removeFromSupernode() + }) + } + + if let emptyNode = self.emptyNode { + transition.animatePosition(layer: emptyNode.layer, from: CGPoint(x: offsetMultiplier.x * layout.size.width, y: offsetMultiplier.y * layout.size.height), to: CGPoint(), removeOnCompletion: true, additive: true) + } + if let previousEmptyNode { + transition.animatePosition(layer: previousEmptyNode.layer, from: CGPoint(), to: CGPoint(x: -offsetMultiplier.x * layout.size.width, y: -offsetMultiplier.y * layout.size.height), removeOnCompletion: false, additive: true, completion: { [weak previousEmptyNode] _ in + previousEmptyNode?.removeFromSupernode() + }) + } + } else { + previousHistoryNode.removeFromSupernode() + previousMessageTransitionNode.removeFromSupernode() + previousLoadingNode.removeFromSupernode() + previousLoadingPlaceholderNode?.removeFromSupernode() + previousEmptyNode?.removeFromSupernode() + } + } else { + transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) + transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) + } + if let blurredHistoryNode = self.blurredHistoryNode { transition.updateFrame(node: blurredHistoryNode, frame: contentBounds) } - - //transition.updateFrame(node: self.historyScrollingArea, frame: contentBounds) + + var isSelectionEnabled = true + if previewing { + isSelectionEnabled = false + } else if case .pinnedMessages = self.chatPresentationInterfaceState.subject { + isSelectionEnabled = false + } else if self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { + isSelectionEnabled = false + } else if case .customChatContents = self.chatLocation { + isSelectionEnabled = false + } + self.historyNode.isSelectionGestureEnabled = isSelectionEnabled transition.updateFrame(node: self.loadingNode, frame: contentBounds) if let loadingPlaceholderNode = self.loadingPlaceholderNode { @@ -2552,7 +2698,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let dismissedTitleAccessoryPanelNode { var dismissedPanelFrame = dismissedTitleAccessoryPanelNode.frame - dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height + transition.updateSublayerTransformOffset(layer: dismissedTitleAccessoryPanelNode.layer, offset: CGPoint(x: 0.0, y: -dismissedPanelFrame.height)) + dismissedPanelFrame.origin.y = titleAccessoryPanelBaseY + dismissedTitleAccessoryPanelNode.clipsToBounds = true + dismissedPanelFrame.size.height = 0.0 + if transition.isAnimated { + dismissedTitleAccessoryPanelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } transition.updateFrame(node: dismissedTitleAccessoryPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedTitleAccessoryPanelNode] _ in dismissedTitleAccessoryPanelNode?.removeFromSupernode() }) @@ -2847,7 +2999,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { displayInlineSearch = true } } - if let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { + if self.chatLocation.threadId == nil, let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { if self.chatPresentationInterfaceState.search != nil { displayInlineSearch = true } @@ -2877,7 +3029,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } else { mappedContents = .empty } - } else if let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { + } else if self.chatLocation.threadId == nil, let channel = self.chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil { mappedContents = .monoforumChats(query: self.chatPresentationInterfaceState.search?.query ?? "") } else if case .peer(self.context.account.peerId) = self.chatPresentationInterfaceState.chatLocation { mappedContents = .tag(MemoryBuffer()) @@ -5078,9 +5230,27 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return nil } ) + + historyNode.position = self.historyNode.position + historyNode.bounds = self.historyNode.bounds + historyNode.transform = self.historyNode.transform + + if let currentListViewLayout = self.currentListViewLayout { + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: currentListViewLayout.size, insets: currentListViewLayout.insets, scrollIndicatorInsets: currentListViewLayout.scrollIndicatorInsets, duration: 0.0, curve: .Default(duration: nil), ensureTopInsetForOverlayHighlightedItems: nil, customAnimationTransition: nil) + historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false, completion: {}) + } + return historyNode } + func prepareSwitchToChatLocation(historyNode: ChatHistoryListNodeImpl, animationDirection: ChatControllerAnimateInnerChatSwitchDirection?) { + self.chatLocation = historyNode.chatLocation + self.pendingSwitchToChatLocation = PendingSwitchToChatLocation( + historyNode: historyNode, + animationDirection: animationDirection + ) + } + func updateChatLocation(chatLocation: ChatLocation, transition: ContainedViewLayoutTransition, tabSwitchDirection: ChatControllerAnimateInnerChatSwitchDirection?) { if chatLocation == self.chatLocation { return diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index dc6721eaef..a7115e2b14 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -61,8 +61,10 @@ extension ChatControllerImpl { var canSendPolls = true if let peer = self.presentationInterfaceState.renderedPeer?.peer { - if let peer = peer as? TelegramUser, peer.botInfo == nil { - canSendPolls = false + if let peer = peer as? TelegramUser { + if peer.botInfo == nil && peer.id != self.context.account.peerId { + canSendPolls = false + } } else if peer is TelegramSecretChat { canSendPolls = false } else if let channel = peer as? TelegramChannel { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift index e58f51508a..6b89d6b1d3 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenCalendarSearch.swift @@ -43,7 +43,7 @@ extension ChatControllerImpl { strongSelf.alwaysShowSearchResultsAsList = false strongSelf.chatDisplayNode.alwaysShowSearchResultsAsList = false strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in - return state.updatedDisplayHistoryFilterAsList(false) + return state.updatedDisplayHistoryFilterAsList(false).updatedSearch(nil) }) c.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index e7928e43d4..57c3f0e91e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -667,7 +667,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto public var contentPositionChanged: (ListViewVisibleContentOffset) -> Void = { _ in } public private(set) var loadState: ChatHistoryNodeLoadState? - private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? + public private(set) var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? private var additionalLoadStateUpdated: [(ChatHistoryNodeLoadState, Bool) -> Void] = [] public private(set) var hasAtLeast3Messages: Bool = false @@ -1830,11 +1830,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let initialData: ChatHistoryCombinedInitialData? switch update.0 { case let .Loading(combinedInitialData, type): - if case .Generic(.FillHole) = type { - applyHole() - return - } - initialData = combinedInitialData if resetScrolling, let previousViewValue = previousView.with({ $0 })?.0 { @@ -1891,13 +1886,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto Queue.mainQueue().async { if let strongSelf = self { - if !strongSelf.didSetInitialData { - strongSelf.didSetInitialData = true - var combinedInitialData = combinedInitialData - combinedInitialData?.cachedData = nil - strongSelf._initialData.set(.single(combinedInitialData)) - } - let cachedData = initialData?.cachedData let cachedDataMessages = initialData?.cachedDataMessages @@ -1917,8 +1905,30 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.currentHistoryState = historyState strongSelf.historyState.set(historyState) } + + if !strongSelf.didSetInitialData { + strongSelf.didSetInitialData = true + var combinedInitialData = combinedInitialData + combinedInitialData?.cachedData = nil + strongSelf._initialData.set(.single(combinedInitialData)) + } + + strongSelf._isReady.set(true) + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + #if DEBUG + let deltaTime = (CFAbsoluteTimeGetCurrent() - strongSelf.initTimestamp) * 1000.0 + print("Chat init to dequeue time: \(deltaTime) ms") + #endif + } } } + + if case .Generic(.FillHole) = type { + applyHole() + return + } + return case let .HistoryView(view, type, scrollPosition, flashIndicators, originalScrollPosition, data, id): if case .Generic(.FillHole) = type { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index f4ada44683..47249ca94e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -230,10 +230,6 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat } func titleTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatTitleAccessoryPanelNode?, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, force: Bool) -> ChatTopicListTitleAccessoryPanelNode? { - //TODO:release - if "".isEmpty { - return nil - } if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.adminRights != nil, chatPresentationInterfaceState.search == nil { let topicListDisplayMode = chatPresentationInterfaceState.topicListDisplayMode ?? .top if case .top = topicListDisplayMode, let peerId = chatPresentationInterfaceState.chatLocation.peerId { diff --git a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift index acf983bd3e..1fb6330ee5 100644 --- a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift @@ -108,12 +108,27 @@ private final class ChatInfoTitlePanelPeerNearbyInfoNode: ASDisplayNode { } final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { + private final class Params { + let width: CGFloat + let leftInset: CGFloat + let rightInset: CGFloat + let interfaceState: ChatPresentationInterfaceState + + init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, interfaceState: ChatPresentationInterfaceState) { + self.width = width + self.leftInset = leftInset + self.rightInset = rightInset + self.interfaceState = interfaceState + } + } + private let context: AccountContext private let separatorNode: ASDisplayNode private let closeButton: HighlightableButtonNode - private var button: UIButton? + private let button: HighlightableButtonNode + private let buttonTitle: ImmediateTextNode private let avatarsContext: AnimatedAvatarSetContext private var avatarsContent: AnimatedAvatarSetContext.Content? @@ -127,6 +142,8 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { private var peers: [EnginePeer] = [] private var count: Int32 = 0 + private var params: Params? + init(context: AccountContext) { self.context = context @@ -137,6 +154,10 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.closeButton.displaysAsynchronously = false + self.button = HighlightableButtonNode() + self.buttonTitle = ImmediateTextNode() + self.buttonTitle.anchorPoint = CGPoint() + self.avatarsContext = AnimatedAvatarSetContext() self.avatarsNode = AnimatedAvatarSetNode() @@ -150,6 +171,12 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) self.addSubnode(self.closeButton) + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.button) + + self.buttonTitle.isUserInteractionEnabled = false + self.button.addSubnode(self.buttonTitle) + self.addSubnode(self.avatarsNode) self.addSubnode(self.activateAreaNode) @@ -161,12 +188,16 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { self.peers = peers self.count = count - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.avatarsContent = self.avatarsContext.update(peers: peers, animated: false) - self.button?.setTitle(presentationData.strings.Conversation_RequestsToJoin(count), for: []) + + if let params = self.params { + let _ = self.updateLayout(width: params.width, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate, interfaceState: params.interfaceState) + } } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult { + self.params = Params(width: width, leftInset: leftInset, rightInset: rightInset, interfaceState: interfaceState) + if interfaceState.theme !== self.theme { self.theme = interfaceState.theme @@ -181,21 +212,15 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) - if self.button == nil { - let view = UIButton() - view.titleLabel?.font = Font.regular(16.0) - view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor, for: []) - view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7), for: [.highlighted]) - view.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) - self.view.addSubview(view) - self.button = view - } + self.buttonTitle.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RequestsToJoin(self.count), font: Font.regular(16.0), textColor: interfaceState.theme.rootController.navigationBar.accentTextColor) - self.button?.setTitle(interfaceState.strings.Conversation_RequestsToJoin(self.count), for: []) + transition.updateFrame(node: self.button, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) - let maxInset = max(contentRightInset, leftInset) - let buttonWidth = floor(width - maxInset * 2.0) - self.button?.frame = CGRect(origin: CGPoint(x: maxInset, y: 0.0), size: CGSize(width: buttonWidth, height: panelHeight)) + let titleSize = self.buttonTitle.updateLayout(CGSize(width: width - leftInset - 90.0 - contentRightInset, height: 100.0)) + var buttonTitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - titleSize.width) * 0.5), y: floor((panelHeight - titleSize.height) * 0.5)), size: titleSize) + buttonTitleFrame.origin.x = max(buttonTitleFrame.minX, leftInset + 90.0) + transition.updatePosition(node: self.buttonTitle, position: buttonTitleFrame.origin) + self.buttonTitle.bounds = CGRect(origin: CGPoint(), size: buttonTitleFrame.size) let initialPanelHeight = panelHeight transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) diff --git a/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift index 2004cc0d1b..f66856418e 100644 --- a/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchInputPanelNode.swift @@ -188,7 +188,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { if case .everything = search.domain { if let _ = interfaceState.renderedPeer?.peer as? TelegramGroup { canSearchMembers = true - } else if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info { + } else if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info, !peer.isMonoForum { canSearchMembers = true } } else { diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index aa3a571d83..27f5f75229 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -191,7 +191,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if case .everything = search.domain { if let _ = params.interfaceState.renderedPeer?.peer as? TelegramGroup { canSearchMembers = true - } else if let peer = params.interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info { + } else if let peer = params.interfaceState.renderedPeer?.peer as? TelegramChannel, case .group = peer.info, !peer.isMonoForum { canSearchMembers = true } } else { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 7cdcc9e4da..95321e6533 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -898,7 +898,7 @@ func openResolvedUrlImpl( func subject(for path: String) -> MediaEditorScreenImpl.Subject? { if path.hasSuffix(".jpg") { - if let image = UIImage(contentsOfFile: path) { + if let image = UIImage(contentsOfFile: path)?.fixedOrientation() { return .image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false) } } else { diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index a65ddc15f8..b41f39b0db 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -528,9 +528,22 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer } fileprivate func proceed() { - let requestPeerType = self.preparedMessage.peerTypes.requestPeerTypes + let peerTypes = self.preparedMessage.peerTypes + var types: [ReplyMarkupButtonRequestPeerType] = [] + if peerTypes.contains(.users) { + types.append(.user(.init(isBot: false, isPremium: nil))) + } + if peerTypes.contains(.bots) { + types.append(.user(.init(isBot: true, isPremium: nil))) + } + if peerTypes.contains(.channels) { + types.append(.channel(.init(isCreator: false, hasUsername: nil, userAdminRights: TelegramChatAdminRights(rights: [.canPostMessages]), botAdminRights: nil))) + } + if peerTypes.contains(.groups) { + types.append(.group(.init(isCreator: false, hasUsername: nil, isForum: nil, botParticipant: false, userAdminRights: nil, botAdminRights: nil))) + } - let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: requestPeerType, hasContactSelector: false, multipleSelection: true, selectForumThreads: true, immediatelyActivateMultipleSelection: true)) + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: types, hasContactSelector: false, multipleSelection: true, selectForumThreads: true, immediatelyActivateMultipleSelection: true)) controller.multiplePeersSelected = { [weak self, weak controller] peers, _, _, _, _, _ in guard let self else {