diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5b1c795769..109dff50d0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15150,3 +15150,10 @@ Error: %8$@"; "ProfileColorSetup.BrowseGiftsForPurchase" = "Browse gifts available for purchase >"; "VoiceChat.RemovedConferencePeerText" = "You removed %@ from this call. They will no longer be able to join using the invite link."; + +"Chat.MessageHeaderBotNewThread" = "New Thread"; +"Chat.MessageContinueLastThread" = "Continue Last Thread"; +"Chat.NewThreadInfo.Title" = "New Thread"; +"Chat.NewThreadInfo.Text" = "Type any message to\ncreate a new thread."; +"Chat.InlineTopicMenu.NewForumThreadTab" = "New Chat"; +"Chat.GenericForuThreadStatus" = "thread"; diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 3afaf1f793..582b45a0e2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2887,7 +2887,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI ), ] - //TODO:localize let (minWidth, buttonsLayout) = actionButtonsLayout( item.context, item.presentationData.theme, @@ -2897,7 +2896,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI ReplyMarkupMessageAttribute( rows: [ ReplyMarkupRow(buttons: [ - ReplyMarkupButton(title: "Continue last thread", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button)) + ReplyMarkupButton(title: item.presentationData.strings.Chat_MessageContinueLastThread, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button)) ]) ], flags: [], diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 1f6409506d..63ccc3c4cf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -347,9 +347,8 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible headerDisplayPeer = ChatMessageDateHeader.HeaderData(contents: .thread(id: threadId, info: threadInfo)) } else if content.firstMessage.threadId == EngineMessage.newTopicThreadId { headerSeparableThreadId = content.firstMessage.threadId - //TODO:localize headerDisplayPeer = ChatMessageDateHeader.HeaderData(contents: .thread(id: threadId, info: Message.AssociatedThreadInfo( - title: "New Chat", + title: presentationData.strings.Chat_MessageHeaderBotNewThread, icon: nil, iconColor: 0, isClosed: false diff --git a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift index 3faae56d1b..0cdd733cb3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift @@ -187,9 +187,8 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0 - //TODO:localize - let titleString = "New Thread" - let subtitleString = "Type any message to\ncreate a new thread." + let titleString = item.presentationData.strings.Chat_NewThreadInfo_Title + let subtitleString = item.presentationData.strings.Chat_NewThreadInfo_Text backgroundSize.height += iconBackgroundSize backgroundSize.height += iconTextSpacing diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift index 820e9376de..8b13789179 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift @@ -1,2316 +1 @@ -import Foundation -import UIKit -import SwiftSignalKit -import Postbox -import TelegramCore -import Display -import AsyncDisplayKit -import TelegramPresentationData -import ChatPresentationInterfaceState -import ComponentFlow -import MultilineTextComponent -import AccountContext -import BlurredBackgroundComponent -import EmojiStatusComponent -import BundleIconComponent -import AvatarNode -import ChatListUI -import ContextUI -import AsyncListComponent -import TextBadgeComponent -import MaskedContainerComponent -import AppBundle -import PresentationDataUtils -/*public final class ChatFloatingTopicsPanel: Component { - public enum Location { - case side - case top - } - - public enum Kind { - case forum - case monoforum - case botForum - } - - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let location: Location - let peerId: EnginePeer.Id - let kind: Kind - let topicId: Int64? - let controller: () -> ViewController? - let togglePanel: () -> Void - let updateTopicId: (Int64?, Bool) -> Void - let openDeletePeer: (Int64) -> Void - - public init( - context: AccountContext, - theme: PresentationTheme, - strings: PresentationStrings, - location: Location, - peerId: EnginePeer.Id, - kind: Kind, - topicId: Int64?, - controller: @escaping () -> ViewController?, - togglePanel: @escaping () -> Void, - updateTopicId: @escaping (Int64?, Bool) -> Void, - openDeletePeer: @escaping (Int64) -> Void - ) { - self.context = context - self.theme = theme - self.strings = strings - self.location = location - self.peerId = peerId - self.kind = kind - self.topicId = topicId - self.controller = controller - self.togglePanel = togglePanel - self.updateTopicId = updateTopicId - self.openDeletePeer = openDeletePeer - } - - public static func ==(lhs: ChatFloatingTopicsPanel, rhs: ChatFloatingTopicsPanel) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.location != rhs.location { - return false - } - if lhs.peerId != rhs.peerId { - return false - } - if lhs.kind != rhs.kind { - return false - } - if lhs.topicId != rhs.topicId { - return false - } - return true - } - - private final class Item: Equatable { - typealias Id = EngineChatList.Item.Id - - let item: EngineChatList.Item - - var id: Id { - return self.item.id - } - - init(item: EngineChatList.Item) { - self.item = item - } - - public static func ==(lhs: Item, rhs: Item) -> Bool { - if lhs === rhs { - return true - } - if lhs.item != rhs.item { - return false - } - return true - } - } - - private protocol ItemComponent: AnyObject { - var item: Item { get } - } - - private final class VerticalItemComponent: Component, ItemComponent { - let context: AccountContext - let item: Item - let isSelected: Bool - let isReordering: Bool - let theme: PresentationTheme - let action: (() -> Void)? - let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? - - init(context: AccountContext, item: Item, isSelected: Bool, isReordering: Bool, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) { - self.context = context - self.item = item - self.isSelected = isSelected - self.isReordering = isReordering - self.theme = theme - self.action = action - self.contextGesture = contextGesture - } - - static func ==(lhs: VerticalItemComponent, rhs: VerticalItemComponent) -> Bool { - if lhs === rhs { - return true - } - if lhs.context !== rhs.context { - return false - } - if lhs.item != rhs.item { - return false - } - if lhs.isSelected != rhs.isSelected { - return false - } - if lhs.isReordering != rhs.isReordering { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if (lhs.action == nil) != (rhs.action == nil) { - return false - } - if (lhs.contextGesture == nil) != (rhs.contextGesture == nil) { - return false - } - return true - } - - final class View: UIView, AsyncListComponent.ItemView { - private let extractedContainerNode: ContextExtractedContentContainingNode - private let containerNode: ContextControllerSourceNode - - private let containerButton: UIView - private var extractedBackgroundView: UIImageView? - - private var tapRecognizer: UITapGestureRecognizer? - - private let iconContainer: MaskedContainerView - private var icon: ComponentView? - private var avatarNode: AvatarNode? - private let title = ComponentView() - private var badge: ComponentView? - - private var component: VerticalItemComponent? - - override init(frame: CGRect) { - self.extractedContainerNode = ContextExtractedContentContainingNode() - self.containerNode = ContextControllerSourceNode() - - self.iconContainer = MaskedContainerView() - self.iconContainer.isUserInteractionEnabled = false - - self.containerButton = UIView() - - super.init(frame: frame) - - self.extractedContainerNode.contentNode.view.addSubview(self.containerButton) - - self.containerNode.addSubnode(self.extractedContainerNode) - self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode - self.addSubview(self.containerNode.view) - - self.containerButton.addSubview(self.iconContainer) - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.tapRecognizer = tapRecognizer - self.containerButton.addGestureRecognizer(tapRecognizer) - tapRecognizer.isEnabled = false - - self.containerNode.activated = { [weak self] gesture, _ in - guard let self, let component = self.component else { - return - } - component.contextGesture?(gesture, self.extractedContainerNode) - } - - self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in - guard let self, let component = self.component else { - return - } - - if isExtracted { - let extractedBackgroundView: UIImageView - if let current = self.extractedBackgroundView { - extractedBackgroundView = current - } else { - extractedBackgroundView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 28.0, color: component.theme.contextMenu.backgroundColor)) - self.extractedBackgroundView = extractedBackgroundView - self.extractedContainerNode.contentNode.view.insertSubview(extractedBackgroundView, at: 0) - extractedBackgroundView.frame = self.extractedContainerNode.contentNode.bounds.insetBy(dx: 2.0, dy: 0.0) - extractedBackgroundView.alpha = 0.0 - } - transition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 1.0) - } else if let extractedBackgroundView = self.extractedBackgroundView { - self.extractedBackgroundView = nil - let alphaTransition: ContainedViewLayoutTransition - if transition.isAnimated { - alphaTransition = .animated(duration: 0.18, curve: .easeInOut) - } else { - alphaTransition = .immediate - } - alphaTransition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 0.0, completion: { [weak extractedBackgroundView] _ in - extractedBackgroundView?.removeFromSuperview() - }) - } - } - - self.containerNode.isGestureEnabled = false - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let iconView = self.icon?.view as? EmojiStatusComponent.View { - iconView.playOnce() - } - self.component?.action?() - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - var mappedPoint = point - if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { - mappedPoint = self.bounds.center - } - return super.hitTest(mappedPoint, with: event) - } - - func isReorderable(at point: CGPoint) -> Bool { - guard let component = self.component else { - return false - } - return component.isReordering - } - - private func updateIsShaking(animated: Bool) { - guard let component = self.component else { - return - } - - if component.isReordering { - if self.layer.animation(forKey: "shaking_position") == nil { - let degreesToRadians: (_ x: CGFloat) -> CGFloat = { x in - return .pi * x / 180.0 - } - - let duration: Double = 0.4 - let displacement: CGFloat = 1.0 - let degreesRotation: CGFloat = 2.0 - - let negativeDisplacement = -1.0 * displacement - let position = CAKeyframeAnimation.init(keyPath: "position") - position.beginTime = 0.8 - position.duration = duration - position.values = [ - NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), - NSValue(cgPoint: CGPoint(x: 0, y: 0)), - NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), - NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), - NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) - ] - position.calculationMode = .linear - position.isRemovedOnCompletion = false - position.repeatCount = Float.greatestFiniteMagnitude - position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) - position.isAdditive = true - - let transform = CAKeyframeAnimation.init(keyPath: "transform") - transform.beginTime = 2.6 - transform.duration = 0.3 - transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) - transform.values = [ - degreesToRadians(-1.0 * degreesRotation), - degreesToRadians(degreesRotation), - degreesToRadians(-1.0 * degreesRotation) - ] - transform.calculationMode = .linear - transform.isRemovedOnCompletion = false - transform.repeatCount = Float.greatestFiniteMagnitude - transform.isAdditive = true - transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) - - self.layer.add(position, forKey: "shaking_position") - self.layer.add(transform, forKey: "shaking_rotation") - } - } else if self.layer.animation(forKey: "shaking_position") != nil { - if let presentationLayer = self.layer.presentation() { - let transition: ComponentTransition = .easeInOut(duration: 0.1) - if presentationLayer.position != self.layer.position { - transition.animatePosition(layer: self.layer, from: CGPoint(x: presentationLayer.position.x - self.layer.position.x, y: presentationLayer.position.y - self.layer.position.y), to: CGPoint(), additive: true) - } - if !CATransform3DIsIdentity(presentationLayer.transform) { - transition.setTransform(layer: self.layer, transform: CATransform3DIdentity) - } - } - - self.layer.removeAnimation(forKey: "shaking_position") - self.layer.removeAnimation(forKey: "shaking_rotation") - } - } - - func update(component: VerticalItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let previousComponent = self.component - self.component = component - - self.tapRecognizer?.isEnabled = component.action != nil - - self.containerNode.isGestureEnabled = component.contextGesture != nil - self.containerNode.activated = { [weak self] gesture, _ in - guard let self, let component = self.component else { - return - } - component.contextGesture?(gesture, self.extractedContainerNode) - } - - let topInset: CGFloat = 8.0 - let bottomInset: CGFloat = 8.0 - let spacing: CGFloat = 3.0 - let iconSize = CGSize(width: 30.0, height: 30.0) - - var avatarIconContent: EmojiStatusComponent.Content? - if case let .forum(topicId) = component.item.item.id { - if topicId != 1, let threadData = component.item.item.threadData { - if let fileId = threadData.info.icon, fileId != 0 { - avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor, loopMode: .count(0)) - } else { - avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) - } - } else { - //newTopicTemplateIcon - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) - } - } - - if let avatarIconContent { - let avatarIconComponent = EmojiStatusComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - content: avatarIconContent, - isVisibleForAnimations: true, - action: nil - ) - let icon: ComponentView - if let current = self.icon { - icon = current - } else { - icon = ComponentView() - self.icon = icon - } - - var iconTransition = transition - if iconTransition.animation.isImmediate, let previousComponent, previousComponent.isSelected != component.isSelected { - iconTransition = .easeInOut(duration: 0.2) - } - - let _ = icon.update( - transition: iconTransition, - component: AnyComponent(avatarIconComponent), - environment: {}, - containerSize: iconSize - ) - } else if let icon = self.icon { - self.icon = nil - icon.view?.removeFromSuperview() - } - - let titleText: String - if case let .forum(topicId) = component.item.item.id { - let _ = topicId - if let threadData = component.item.item.threadData { - titleText = threadData.info.title - } else { - titleText = " " - } - } else { - titleText = component.item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " - } - - if let avatarIconContent, let icon = self.icon { - let avatarIconComponent = EmojiStatusComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - content: avatarIconContent, - isVisibleForAnimations: true, - action: nil - ) - let _ = icon.update( - transition: .immediate, - component: AnyComponent(avatarIconComponent), - environment: {}, - containerSize: iconSize - ) - } - - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 2 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - 6.0 * 2.0, height: 100.0) - ) - - let contentSize: CGFloat = topInset + bottomInset + iconSize.height + spacing + titleSize.height - let size = CGSize(width: availableSize.width, height: contentSize) - - let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: topInset), size: iconSize) - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) - - self.iconContainer.frame = iconFrame - - if let icon = self.icon { - if let avatarNode = self.avatarNode { - self.avatarNode = nil - avatarNode.view.removeFromSuperview() - } - - if let iconView = icon.view { - if iconView.superview == nil { - iconView.isUserInteractionEnabled = false - self.iconContainer.contentView.addSubview(iconView) - } - iconView.frame = CGRect(origin: CGPoint(), size: iconFrame.size) - } - } else { - let avatarNode: AvatarNode - if let current = self.avatarNode { - avatarNode = current - } else { - avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) - avatarNode.isUserInteractionEnabled = false - self.avatarNode = avatarNode - self.iconContainer.contentView.addSubview(avatarNode.view) - } - avatarNode.frame = CGRect(origin: CGPoint(), size: iconFrame.size) - avatarNode.updateSize(size: iconFrame.size) - - if let peer = component.item.item.renderedPeer.chatMainPeer { - if peer.smallProfileImage != nil { - avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } else { - avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } - } - } - - var iconMaskItems: [MaskedContainerView.Item] = [] - if let readCounters = component.item.item.readCounters, readCounters.count > 0 { - let badge: ComponentView - var badgeTransition = transition - if let current = self.badge { - badge = current - } else { - badgeTransition = .immediate - badge = ComponentView() - self.badge = badge - } - - let badgeSize = badge.update( - transition: badgeTransition, - component: AnyComponent(TextBadgeComponent( - text: countString(Int64(readCounters.count)), - font: Font.medium(12.0), - background: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveBackgroundColor : component.theme.chatList.unreadBadgeActiveBackgroundColor, - foreground: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveTextColor : component.theme.chatList.unreadBadgeActiveTextColor, - insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - let badgeFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 10.0 - badgeSize.width, y: iconFrame.minY - 6.0), size: badgeSize) - if let badgeView = badge.view { - if badgeView.superview == nil { - self.containerButton.addSubview(badgeView) - } - badgeView.frame = badgeFrame - } - let badgeMaskFrame = badgeFrame.offsetBy(dx: -iconFrame.minX, dy: -iconFrame.minY).insetBy(dx: -1.33, dy: -1.33) - iconMaskItems.append(MaskedContainerView.Item( - frame: badgeMaskFrame, - shape: .roundedRect(cornerRadius: badgeMaskFrame.height * 0.5) - )) - } else if let badge = self.badge { - self.badge = nil - badge.view?.removeFromSuperview() - } - self.iconContainer.update(size: iconFrame.size, items: iconMaskItems, isInverted: true) - self.iconContainer.frame = iconFrame - - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.containerButton.addSubview(titleView) - } - titleView.frame = titleFrame - } - - transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) - - self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - - self.updateIsShaking(animated: !transition.animation.isImmediate) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } - } - - private final class HorizontalItemComponent: Component, ItemComponent { - let context: AccountContext - let item: Item - let isSelected: Bool - let isReordering: Bool - let theme: PresentationTheme - let action: (() -> Void)? - let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? - - init(context: AccountContext, item: Item, isSelected: Bool, isReordering: Bool, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) { - self.context = context - self.item = item - self.isSelected = isSelected - self.isReordering = isReordering - self.theme = theme - self.action = action - self.contextGesture = contextGesture - } - - static func ==(lhs: HorizontalItemComponent, rhs: HorizontalItemComponent) -> Bool { - if lhs === rhs { - return true - } - if lhs.context !== rhs.context { - return false - } - if lhs.item != rhs.item { - return false - } - if lhs.isSelected != rhs.isSelected { - return false - } - if lhs.isReordering != rhs.isReordering { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if (lhs.action == nil) != (rhs.action == nil) { - return false - } - if (lhs.contextGesture == nil) != (rhs.contextGesture == nil) { - return false - } - return true - } - - final class View: UIView, AsyncListComponent.ItemView { - private let extractedContainerNode: ContextExtractedContentContainingNode - private let containerNode: ContextControllerSourceNode - - private let containerButton: UIView - private var extractedBackgroundView: UIImageView? - - private var tapRecognizer: UITapGestureRecognizer? - - private var icon: ComponentView? - private var avatarNode: AvatarNode? - private let title = ComponentView() - private var badge: ComponentView? - - private var component: HorizontalItemComponent? - - override init(frame: CGRect) { - self.extractedContainerNode = ContextExtractedContentContainingNode() - self.containerNode = ContextControllerSourceNode() - - self.containerButton = UIView() - - super.init(frame: frame) - - self.extractedContainerNode.contentNode.view.addSubview(self.containerButton) - - self.containerNode.addSubnode(self.extractedContainerNode) - self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode - self.addSubview(self.containerNode.view) - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.tapRecognizer = tapRecognizer - self.containerButton.addGestureRecognizer(tapRecognizer) - tapRecognizer.isEnabled = false - - self.containerNode.activated = { [weak self] gesture, _ in - guard let self, let component = self.component else { - return - } - component.contextGesture?(gesture, self.extractedContainerNode) - } - - self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in - guard let self, let component = self.component else { - return - } - - if isExtracted { - let extractedBackgroundView: UIImageView - if let current = self.extractedBackgroundView { - extractedBackgroundView = current - } else { - extractedBackgroundView = UIImageView(image: generateStretchableFilledCircleImage(diameter: 28.0, color: component.theme.contextMenu.backgroundColor)) - self.extractedBackgroundView = extractedBackgroundView - self.extractedContainerNode.contentNode.view.insertSubview(extractedBackgroundView, at: 0) - extractedBackgroundView.frame = self.extractedContainerNode.contentNode.bounds.insetBy(dx: 2.0, dy: 0.0) - extractedBackgroundView.alpha = 0.0 - } - transition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 1.0) - } else if let extractedBackgroundView = self.extractedBackgroundView { - self.extractedBackgroundView = nil - let alphaTransition: ContainedViewLayoutTransition - if transition.isAnimated { - alphaTransition = .animated(duration: 0.18, curve: .easeInOut) - } else { - alphaTransition = .immediate - } - alphaTransition.updateAlpha(layer: extractedBackgroundView.layer, alpha: 0.0, completion: { [weak extractedBackgroundView] _ in - extractedBackgroundView?.removeFromSuperview() - }) - } - } - - self.containerNode.isGestureEnabled = false - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let iconView = self.icon?.view as? EmojiStatusComponent.View { - iconView.playOnce() - } - self.component?.action?() - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - var mappedPoint = point - if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { - mappedPoint = self.bounds.center - } - return super.hitTest(mappedPoint, with: event) - } - - func isReorderable(at point: CGPoint) -> Bool { - guard let component = self.component else { - return false - } - return component.isReordering - } - - private func updateIsShaking(animated: Bool) { - guard let component = self.component else { - return - } - - if component.isReordering { - if self.layer.animation(forKey: "shaking_position") == nil { - let degreesToRadians: (_ x: CGFloat) -> CGFloat = { x in - return .pi * x / 180.0 - } - - let duration: Double = 0.4 - let displacement: CGFloat = 1.0 - let degreesRotation: CGFloat = 2.0 - - let negativeDisplacement = -1.0 * displacement - let position = CAKeyframeAnimation.init(keyPath: "position") - position.beginTime = 0.8 - position.duration = duration - position.values = [ - NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), - NSValue(cgPoint: CGPoint(x: 0, y: 0)), - NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), - NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), - NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) - ] - position.calculationMode = .linear - position.isRemovedOnCompletion = false - position.repeatCount = Float.greatestFiniteMagnitude - position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) - position.isAdditive = true - - let transform = CAKeyframeAnimation.init(keyPath: "transform") - transform.beginTime = 2.6 - transform.duration = 0.3 - transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) - transform.values = [ - degreesToRadians(-1.0 * degreesRotation), - degreesToRadians(degreesRotation), - degreesToRadians(-1.0 * degreesRotation) - ] - transform.calculationMode = .linear - transform.isRemovedOnCompletion = false - transform.repeatCount = Float.greatestFiniteMagnitude - transform.isAdditive = true - transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) - - self.layer.add(position, forKey: "shaking_position") - self.layer.add(transform, forKey: "shaking_rotation") - } - } else if self.layer.animation(forKey: "shaking_position") != nil { - if let presentationLayer = self.layer.presentation() { - let transition: ComponentTransition = .easeInOut(duration: 0.1) - if presentationLayer.position != self.layer.position { - transition.animatePosition(layer: self.layer, from: CGPoint(x: presentationLayer.position.x - self.layer.position.x, y: presentationLayer.position.y - self.layer.position.y), to: CGPoint(), additive: true) - } - if !CATransform3DIsIdentity(presentationLayer.transform) { - transition.setTransform(layer: self.layer, transform: CATransform3DIdentity) - } - } - - self.layer.removeAnimation(forKey: "shaking_position") - self.layer.removeAnimation(forKey: "shaking_rotation") - } - } - - func update(component: HorizontalItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - - self.tapRecognizer?.isEnabled = component.action != nil - - self.containerNode.isGestureEnabled = component.contextGesture != nil - self.containerNode.activated = { [weak self] gesture, _ in - guard let self, let component = self.component else { - return - } - component.contextGesture?(gesture, self.extractedContainerNode) - } - - let leftInset: CGFloat = 12.0 - let rightInset: CGFloat = 12.0 - let spacing: CGFloat = 4.0 - let badgeSpacing: CGFloat = 4.0 - let iconSize = CGSize(width: 18.0, height: 18.0) - - var avatarIconContent: EmojiStatusComponent.Content? - if case let .forum(topicId) = component.item.item.id { - if topicId != 1, let threadData = component.item.item.threadData { - if let fileId = threadData.info.icon, fileId != 0 { - avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor, loopMode: .count(0)) - } else { - avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) - } - } else { - //newTopicTemplateIcon - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) - } - } - - if let avatarIconContent { - let avatarIconComponent = EmojiStatusComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - content: avatarIconContent, - isVisibleForAnimations: false, - action: nil - ) - let icon: ComponentView - if let current = self.icon { - icon = current - } else { - icon = ComponentView() - self.icon = icon - } - let _ = icon.update( - transition: .immediate, - component: AnyComponent(avatarIconComponent), - environment: {}, - containerSize: iconSize - ) - } else if let icon = self.icon { - self.icon = nil - icon.view?.removeFromSuperview() - } - - let titleText: String - if case let .forum(topicId) = component.item.item.id { - let _ = topicId - if let threadData = component.item.item.threadData { - titleText = threadData.info.title - } else { - titleText = " " - } - } else { - titleText = component.item.item.renderedPeer.chatMainPeer?.compactDisplayTitle ?? " " - } - - if let avatarIconContent, let icon = self.icon { - let avatarIconComponent = EmojiStatusComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - content: avatarIconContent, - isVisibleForAnimations: false, - action: nil - ) - let _ = icon.update( - transition: .immediate, - component: AnyComponent(avatarIconComponent), - environment: {}, - containerSize: iconSize - ) - } - - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 2 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - 6.0 * 2.0, height: 100.0) - ) - - var badgeSize: CGSize? - if let readCounters = component.item.item.readCounters, readCounters.count > 0 { - let badge: ComponentView - var badgeTransition = transition - if let current = self.badge { - badge = current - } else { - badgeTransition = .immediate - badge = ComponentView() - self.badge = badge - } - - badgeSize = badge.update( - transition: badgeTransition, - component: AnyComponent(TextBadgeComponent( - text: countString(Int64(readCounters.count)), - font: Font.medium(12.0), - background: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveBackgroundColor : component.theme.chatList.unreadBadgeActiveBackgroundColor, - foreground: component.item.item.isMuted ? component.theme.chatList.unreadBadgeInactiveTextColor : component.theme.chatList.unreadBadgeActiveTextColor, - insets: UIEdgeInsets(top: 1.0, left: 5.0, bottom: 2.0, right: 5.0) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - } else if let badge = self.badge { - self.badge = nil - badge.view?.removeFromSuperview() - } - - var contentSize: CGFloat = leftInset + rightInset + iconSize.width + spacing + titleSize.width - if let badgeSize { - contentSize += badgeSize.width + badgeSpacing - } - let size = CGSize(width: contentSize, height: availableSize.height) - - let iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) - let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) - - if let icon = self.icon { - if let avatarNode = self.avatarNode { - self.avatarNode = nil - avatarNode.view.removeFromSuperview() - } - - if let iconView = icon.view { - if iconView.superview == nil { - iconView.isUserInteractionEnabled = false - self.containerButton.addSubview(iconView) - } - iconView.frame = iconFrame - } - } else { - let avatarNode: AvatarNode - if let current = self.avatarNode { - avatarNode = current - } else { - avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) - avatarNode.isUserInteractionEnabled = false - self.avatarNode = avatarNode - self.containerButton.addSubview(avatarNode.view) - } - avatarNode.frame = iconFrame - avatarNode.updateSize(size: iconFrame.size) - - if let peer = component.item.item.renderedPeer.chatMainPeer { - if peer.smallProfileImage != nil { - avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } else { - avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: nil, emptyColor: .gray, clipStyle: .round, synchronousLoad: false, displayDimensions: iconFrame.size) - } - } - } - - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.containerButton.addSubview(titleView) - } - titleView.frame = titleFrame - } - - if let badge = self.badge, let badgeSize { - let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) * 0.5)), size: badgeSize) - if let badgeView = badge.view { - if badgeView.superview == nil { - self.containerButton.addSubview(badgeView) - } - badgeView.frame = badgeFrame - } - } - - transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) - - self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - - self.updateIsShaking(animated: !transition.animation.isImmediate) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } - } - - private final class TabItemView: UIView { - private let context: AccountContext - private let action: () -> Void - - private let extractedContainerNode: ContextExtractedContentContainingNode - private let containerNode: ContextControllerSourceNode - - private let containerButton: HighlightTrackingButton - - private var icon = ComponentView() - - private var isReordering: Bool = false - - init(context: AccountContext, action: @escaping (() -> Void)) { - self.context = context - self.action = action - - self.extractedContainerNode = ContextExtractedContentContainingNode() - self.containerNode = ContextControllerSourceNode() - - self.containerButton = HighlightTrackingButton() - - super.init(frame: CGRect()) - - self.extractedContainerNode.contentNode.view.addSubview(self.containerButton) - - self.containerNode.addSubnode(self.extractedContainerNode) - self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode - self.addSubview(self.containerNode.view) - - self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - self.containerButton.highligthedChanged = { [weak self] highlighted in - if let self, self.bounds.width > 0.0 { - let topScale: CGFloat = (self.bounds.width - 1.0) / self.bounds.width - let maxScale: CGFloat = (self.bounds.width + 1.0) / self.bounds.width - - if highlighted { - self.layer.removeAnimation(forKey: "opacity") - self.layer.removeAnimation(forKey: "sublayerTransform") - let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) - transition.updateTransformScale(layer: self.layer, scale: topScale) - } else { - let transition: ContainedViewLayoutTransition = .immediate - transition.updateTransformScale(layer: self.layer, scale: 1.0) - - self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in - guard let self else { - return - } - - self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) - }) - } - } - } - - self.containerNode.isGestureEnabled = false - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - @objc private func pressed() { - self.action() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - - func update(context: AccountContext, theme: PresentationTheme, width: CGFloat, location: Location, isReordering: Bool, transition: ComponentTransition) -> CGSize { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) - - var animateIconIn = false - if self.isReordering != isReordering { - self.isReordering = isReordering - if let iconView = self.icon.view { - self.icon = ComponentView() - transition.setScale(view: iconView, scale: 0.001) - alphaTransition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in - iconView?.removeFromSuperview() - }) - animateIconIn = true - } - } - - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent( - name: isReordering ? "Media Editor/Done" : "Chat/Title Panels/SidebarIcon", - tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.rootController.navigationBar.secondaryTextColor, - maxSize: CGSize(width: 24.0, height: 24.0), - scaleFactor: 1.0 - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - - let topInset: CGFloat = 10.0 - let bottomInset: CGFloat = 2.0 - - let contentSize: CGFloat = topInset + iconSize.height + bottomInset - let size = CGSize(width: width, height: contentSize) - - let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: topInset), size: iconSize) - - if let iconView = self.icon.view { - if iconView.superview == nil { - iconView.isUserInteractionEnabled = false - self.containerButton.addSubview(iconView) - } - iconView.frame = iconFrame - if animateIconIn { - alphaTransition.animateAlpha(view: iconView, from: 0.0, to: 1.0) - transition.animateScale(view: iconView, from: 0.001, to: 1.0) - } - } - - transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) - - self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - self.containerNode.frame = CGRect(origin: CGPoint(), size: size) - - return size - } - } - - private protocol AllItemComponent: AnyObject { - } - - private final class VerticalAllItemComponent: Component, AllItemComponent { - let isSelected: Bool - let kind: ChatFloatingTopicsPanel.Kind - let theme: PresentationTheme - let strings: PresentationStrings - let action: (() -> Void)? - - init(isSelected: Bool, kind: ChatFloatingTopicsPanel.Kind, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { - self.isSelected = isSelected - self.kind = kind - self.theme = theme - self.strings = strings - self.action = action - } - - static func ==(lhs: VerticalAllItemComponent, rhs: VerticalAllItemComponent) -> Bool { - if lhs === rhs { - return true - } - if lhs.isSelected != rhs.isSelected { - return false - } - if lhs.kind != rhs.kind { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if (lhs.action == nil) != (rhs.action == nil) { - return false - } - return true - } - - final class View: UIView { - private let containerButton: UIView - - private let icon = ComponentView() - private let title = ComponentView() - - private var tapRecognizer: UITapGestureRecognizer? - - private var component: VerticalAllItemComponent? - - override init(frame: CGRect) { - self.containerButton = UIView() - - super.init(frame: frame) - - self.addSubview(self.containerButton) - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.tapRecognizer = tapRecognizer - self.containerButton.addGestureRecognizer(tapRecognizer) - tapRecognizer.isEnabled = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.component?.action?() - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - var mappedPoint = point - if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { - mappedPoint = self.bounds.center - } - return super.hitTest(mappedPoint, with: event) - } - - func update(component: VerticalAllItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - - self.tapRecognizer?.isEnabled = component.action != nil - - let topInset: CGFloat = 6.0 - let bottomInset: CGFloat = 8.0 - - let spacing: CGFloat = 1.0 - - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent( - name: "Chat List/Tabs/IconChats", - tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - - let titleText: String - if case .botForum = component.kind { - //TODO:localize - titleText = "New Chat" - } else { - titleText = component.strings.Chat_InlineTopicMenu_AllTab - } - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), - maximumNumberOfLines: 2 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - 4.0 * 2.0, height: 100.0) - ) - - let contentSize: CGFloat = topInset + bottomInset + iconSize.height + spacing + titleSize.height - let size = CGSize(width: availableSize.width, height: contentSize) - - let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: topInset), size: iconSize) - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: iconFrame.maxY + spacing), size: titleSize) - - if let iconView = self.icon.view { - if iconView.superview == nil { - iconView.isUserInteractionEnabled = false - self.containerButton.addSubview(iconView) - } - iconView.frame = iconFrame - } - - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.containerButton.addSubview(titleView) - } - titleView.frame = titleFrame - } - - transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } - } - - private final class HorizontalAllItemComponent: Component, AllItemComponent { - let isSelected: Bool - let kind: ChatFloatingTopicsPanel.Kind - let theme: PresentationTheme - let strings: PresentationStrings - let action: (() -> Void)? - - init(isSelected: Bool, kind: ChatFloatingTopicsPanel.Kind, theme: PresentationTheme, strings: PresentationStrings, action: (() -> Void)?) { - self.isSelected = isSelected - self.kind = kind - self.theme = theme - self.strings = strings - self.action = action - } - - static func ==(lhs: HorizontalAllItemComponent, rhs: HorizontalAllItemComponent) -> Bool { - if lhs === rhs { - return true - } - if lhs.isSelected != rhs.isSelected { - return false - } - if lhs.kind != rhs.kind { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if (lhs.action == nil) != (rhs.action == nil) { - return false - } - return true - } - - final class View: UIView { - private let containerButton: UIView - - private let title = ComponentView() - - private var tapRecognizer: UITapGestureRecognizer? - - private var component: HorizontalAllItemComponent? - - override init(frame: CGRect) { - self.containerButton = UIView() - - super.init(frame: frame) - - self.addSubview(self.containerButton) - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) - self.tapRecognizer = tapRecognizer - self.containerButton.addGestureRecognizer(tapRecognizer) - tapRecognizer.isEnabled = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.component?.action?() - } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - var mappedPoint = point - if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) { - mappedPoint = self.bounds.center - } - return super.hitTest(mappedPoint, with: event) - } - - func update(component: HorizontalAllItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - - self.tapRecognizer?.isEnabled = component.action != nil - - let leftInset: CGFloat = 6.0 - let rightInset: CGFloat = 12.0 - - let titleText: String - if case .botForum = component.kind { - //TODO:localize - titleText = "New Chat" - } else { - titleText = component.strings.Chat_InlineTopicMenu_AllTab - } - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)), - maximumNumberOfLines: 2 - )), - environment: {}, - containerSize: CGSize(width: 400.0, height: 200.0) - ) - - let contentSize: CGFloat = leftInset + rightInset + titleSize.width - let size = CGSize(width: contentSize, height: availableSize.height) - - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) - - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.containerButton.addSubview(titleView) - } - titleView.frame = titleFrame - } - - transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: size)) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } - } - - private enum ScrollId: Hashable { - case all - case topic(Int64) - } - - public final class View: UIView { - private let list = ComponentView() - private let listState = AsyncListComponent.ExternalState() - private let scrollContainerView: UIView - private let scrollViewMask: UIImageView - - private var background: ComponentView? - private var separatorLayer: SimpleLayer? - - private let selectedLineContainer: AsyncListComponent.OverlayContainerView - private let selectedLineView: UIImageView - private let pinnedBackgroundContainer: AsyncListComponent.OverlayContainerView - private let pinnedBackgroundView: UIImageView - private let pinnedIconView: UIImageView - - private var tabItemView: TabItemView? - - private var peerId: EnginePeer.Id? - private var rawItems: [Item] = [] - private var reorderingItems: [Item]? - private var resetReorderingOnNextUpdate: Bool = false - private var itemsContentVersion: Int = 0 - - private var isTogglingPinnedItem: Bool = false - private weak var dismissContextControllerOnNextUpdate: ContextController? - - private var component: ChatFloatingTopicsPanel? - private weak var state: EmptyComponentState? - private var isUpdating: Bool = false - - private var appliedScrollToId: ScrollId? - private var isReordering: Bool = false - - private var itemsDisposable: Disposable? - - override public init(frame: CGRect) { - self.selectedLineView = UIImageView() - self.selectedLineView.isHidden = true - self.selectedLineContainer = AsyncListComponent.OverlayContainerView() - self.selectedLineContainer.addSubview(self.selectedLineView) - - self.pinnedIconView = UIImageView() - self.pinnedBackgroundView = UIImageView() - self.pinnedBackgroundContainer = AsyncListComponent.OverlayContainerView() - self.pinnedBackgroundContainer.addSubview(self.pinnedIconView) - self.pinnedBackgroundContainer.addSubview(self.pinnedBackgroundView) - self.pinnedBackgroundContainer.isHidden = true - - self.scrollContainerView = UIView() - self.scrollViewMask = UIImageView() - self.scrollContainerView.mask = self.scrollViewMask - - super.init(frame: frame) - - self.addSubview(self.scrollContainerView) - self.scrollContainerView.addSubview(self.pinnedBackgroundContainer) - self.scrollContainerView.addSubview(self.selectedLineContainer) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.itemsDisposable?.dispose() - } - - public func updateGlobalOffset(globalOffset: CGFloat, transition: ComponentTransition) { - guard let component = self.component else { - return - } - if let tabItemView = self.tabItemView { - switch component.location { - case .side: - transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(-globalOffset, 0.0, 0.0)) - case .top: - transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0)) - } - } - } - - public func topicIndex(threadId: Int64?) -> Int? { - if let threadId { - if let value = self.rawItems.firstIndex(where: { item in - if item.id == .chatList(PeerId(threadId)) { - return true - } else if item.id == .forum(threadId) { - return true - } else { - return false - } - }) { - return value + 1 - } else { - return nil - } - } else { - return 0 - } - } - - private func updateListOverlays(visibleItems: AsyncListComponent.VisibleItems, transition: ComponentTransition) { - guard let component = self.component, let listView = self.list.view else { - return - } - - var selectedItemFrame: CGRect? - var beforePinnedItemsPosition: CGFloat? - var afterPinnedItemsPosition: CGFloat? - var seenPinnedItems = false - for item in visibleItems { - if let _ = item.item.component.wrapped as? AllItemComponent { - if component.topicId == nil { - switch component.location { - case .side: - selectedItemFrame = item.frame - case .top: - selectedItemFrame = CGRect(origin: CGPoint(x: item.frame.minX + 5.0, y: item.frame.minY), size: CGSize(width: item.frame.width - 4.0 - 11.0, height: item.frame.height)) - } - } - if !seenPinnedItems { - switch component.location { - case .side: - beforePinnedItemsPosition = item.frame.maxY - case .top: - beforePinnedItemsPosition = item.frame.maxX - } - } - } else if let itemComponent = item.item.component.wrapped as? ItemComponent { - let topicId: Int64 - switch itemComponent.item.item.id { - case let .chatList(peerId): - topicId = peerId.toInt64() - case let .forum(topicIdValue): - topicId = topicIdValue - } - if topicId == component.topicId { - selectedItemFrame = item.frame - } - - var isPinned = false - if case let .forum(pinnedIndex, _, _, _, _) = itemComponent.item.item.index { - if case .index = pinnedIndex { - isPinned = true - } - } - if isPinned { - seenPinnedItems = true - } else { - if !seenPinnedItems { - switch component.location { - case .side: - beforePinnedItemsPosition = item.frame.maxY - case .top: - beforePinnedItemsPosition = item.frame.maxX - } - } else { - if afterPinnedItemsPosition == nil { - switch component.location { - case .side: - afterPinnedItemsPosition = item.frame.minY - case .top: - afterPinnedItemsPosition = item.frame.minX - } - } - } - } - } - } - - if seenPinnedItems { - if beforePinnedItemsPosition == nil { - beforePinnedItemsPosition = -500.0 - } - if afterPinnedItemsPosition == nil { - switch component.location { - case .side: - afterPinnedItemsPosition = listView.bounds.height + 500.0 - case .top: - afterPinnedItemsPosition = listView.bounds.width + 500.0 - } - } - } - - if let selectedItemFrame { - var lineTransition = transition - if self.selectedLineView.isHidden { - self.selectedLineView.isHidden = false - lineTransition = .immediate - } - let selectedLineFrame: CGRect - switch component.location { - case .side: - selectedLineFrame = CGRect(origin: CGPoint(x: 0.0, y: selectedItemFrame.minY), size: CGSize(width: 4.0, height: selectedItemFrame.height)) - case .top: - selectedLineFrame = CGRect(origin: CGPoint(x: selectedItemFrame.minX, y: listView.frame.maxY - 3.0), size: CGSize(width: selectedItemFrame.width, height: 3.0)) - } - - self.selectedLineContainer.updatePosition(position: selectedLineFrame.origin, transition: lineTransition) - lineTransition.setFrame(view: self.selectedLineView, frame: CGRect(origin: CGPoint(), size: selectedLineFrame.size)) - } else { - self.selectedLineView.isHidden = true - } - - if let beforePinnedItemsPosition, let afterPinnedItemsPosition, afterPinnedItemsPosition > beforePinnedItemsPosition { - var pinnedItemsTransition = transition - if self.pinnedBackgroundContainer.isHidden { - self.pinnedBackgroundContainer.isHidden = false - pinnedItemsTransition = .immediate - } - let pinnedItemsBackgroundFrame: CGRect - switch component.location { - case .side: - pinnedItemsBackgroundFrame = CGRect(origin: CGPoint(x: 5.0, y: beforePinnedItemsPosition), size: CGSize(width: listView.bounds.width - 5.0 - 4.0, height: afterPinnedItemsPosition - beforePinnedItemsPosition)) - case .top: - pinnedItemsBackgroundFrame = CGRect(origin: CGPoint(x: beforePinnedItemsPosition, y: 4.0), size: CGSize(width: afterPinnedItemsPosition - beforePinnedItemsPosition, height: listView.bounds.height - 5.0 - 4.0)) - } - self.pinnedBackgroundContainer.updatePosition(position: pinnedItemsBackgroundFrame.origin, transition: pinnedItemsTransition) - pinnedItemsTransition.setFrame(view: self.pinnedBackgroundView, frame: CGRect(origin: CGPoint(), size: pinnedItemsBackgroundFrame.size)) - - let pinnedIconFrame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 12.0, height: 12.0)) - pinnedItemsTransition.setFrame(view: self.pinnedIconView, frame: pinnedIconFrame) - } else { - self.pinnedBackgroundContainer.isHidden = true - } - } - - private func updateIsReordering(isReordering: Bool) { - self.isReordering = isReordering - if !self.isUpdating { - self.state?.updated(transition: .spring(duration: 0.4)) - } - } - - func update(component: ChatFloatingTopicsPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - self.state = state - - if self.resetReorderingOnNextUpdate { - self.resetReorderingOnNextUpdate = false - self.reorderingItems = nil - self.isReordering = false - } - - if self.component == nil { - let threadListSignal: Signal<(EnginePeer.Id, EngineChatList), NoError> - - switch component.kind { - default: - let defaultPeerId = component.peerId - threadListSignal = component.context.sharedContext.subscribeChatListData(context: component.context, location: component.kind == .monoforum ? .savedMessagesChats(peerId: component.peerId) : .forum(peerId: component.peerId)) - |> map { value in - return (defaultPeerId, value) - } - } - - self.itemsDisposable = (threadListSignal - |> deliverOnMainQueue).startStrict(next: { [weak self] peerId, chatList in - guard let self, let _ = self.component else { - return - } - - self.peerId = peerId - - let wasEmpty = self.rawItems.isEmpty - - self.rawItems.removeAll() - for item in chatList.items.reversed() { - self.rawItems.append(Item(item: item)) - } - - if self.reorderingItems != nil { - self.reorderingItems = self.rawItems - } - - if !self.isUpdating { - self.state?.updated(transition: (wasEmpty || self.isTogglingPinnedItem) ? .immediate : .spring(duration: 0.4)) - } - }) - - switch component.location { - case .side: - self.scrollViewMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ - UIColor(white: 1.0, alpha: 0.0), - UIColor(white: 1.0, alpha: 1.0) - ], locations: [0.0, 1.0], direction: .vertical)?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 8) - case .top: - self.scrollViewMask.image = generateGradientImage(size: CGSize(width: 8.0, height: 8.0), colors: [ - UIColor(white: 1.0, alpha: 0.0), - UIColor(white: 1.0, alpha: 1.0) - ], locations: [0.0, 1.0], direction: .horizontal)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 0) - } - } - let themeUpdated = self.component?.theme !== component.theme - self.component = component - - if case .side = component.location { - let background: ComponentView - if let current = self.background { - background = current - } else { - background = ComponentView() - self.background = background - } - let _ = background.update( - transition: transition, - component: AnyComponent(BlurredBackgroundComponent( - color: component.theme.rootController.navigationBar.blurredBackgroundColor - )), - environment: {}, - containerSize: availableSize - ) - - if let backgroundView = background.view { - if backgroundView.superview == nil { - self.insertSubview(backgroundView, at: 0) - } - transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) - } - - let separatorLayer: SimpleLayer - if let current = self.separatorLayer { - separatorLayer = current - } else { - separatorLayer = SimpleLayer() - self.separatorLayer = separatorLayer - self.layer.addSublayer(separatorLayer) - } - if themeUpdated { - separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor - } - - transition.setFrame(layer: separatorLayer, frame: CGRect(origin: CGPoint(x: availableSize.width, y: 0.0), size: CGSize(width: UIScreenPixel, height: availableSize.height))) - } - - if themeUpdated { - switch component.location { - case .side: - self.selectedLineView.image = generateImage(CGSize(width: 4.0, height: 7.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(component.theme.rootController.navigationBar.accentTextColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) - })?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 4) - case .top: - self.selectedLineView.image = generateImage(CGSize(width: 4.0, height: 3.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(component.theme.rootController.navigationBar.accentTextColor.cgColor) - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height * 2.0)), cornerRadius: 2.0).cgPath) - context.fillPath() - })?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 1) - } - - if self.pinnedIconView.image == nil { - self.pinnedIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/Pinned"), color: .white)?.withRenderingMode(.alwaysTemplate) - } - self.pinnedIconView.tintColor = component.theme.chatList.unreadBadgeInactiveBackgroundColor - - if self.pinnedBackgroundView.image == nil { - self.pinnedBackgroundView.image = generateStretchableFilledCircleImage(diameter: 10.0, color: .white)?.withRenderingMode(.alwaysTemplate) - } - var pinnedBackgroundColor = component.theme.rootController.navigationSearchBar.inputFillColor - if pinnedBackgroundColor.distance(to: component.theme.list.blocksBackgroundColor) < 100 { - pinnedBackgroundColor = pinnedBackgroundColor.withMultipliedBrightnessBy(0.8) - } - self.pinnedBackgroundView.tintColor = pinnedBackgroundColor - } - - let environment = environment[EnvironmentType.self].value - - let containerInsets = environment.insets - - var directionContainerInset: CGFloat - switch component.location { - case .side: - directionContainerInset = containerInsets.top - case .top: - directionContainerInset = containerInsets.left - } - - do { - var itemTransition = transition - var animateIn = false - let itemView: TabItemView - if let current = self.tabItemView { - itemView = current - } else { - itemTransition = .immediate - animateIn = true - itemView = TabItemView(context: component.context, action: { [weak self] in - guard let self, let peerId = self.peerId, let component = self.component else { - return - } - if self.isReordering { - if let reorderingItems = self.reorderingItems { - var threadIds: [Int64] = [] - for item in reorderingItems { - if case let .forum(pinnedIndex, _, threadId, _, _) = item.item.index, case .index = pinnedIndex { - threadIds.append(threadId) - } - } - - var currentThreadIds: [Int64] = [] - for item in self.rawItems { - if case let .forum(pinnedIndex, _, threadId, _, _) = item.item.index, case .index = pinnedIndex { - currentThreadIds.append(threadId) - } - } - - if threadIds != currentThreadIds { - let _ = component.context.engine.peers.setForumChannelPinnedTopics(id: peerId, threadIds: threadIds).startStandalone() - self.resetReorderingOnNextUpdate = true - } else { - self.reorderingItems = nil - self.isReordering = false - self.state?.updated(transition: .spring(duration: 0.4)) - } - } else { - self.isReordering = false - self.state?.updated(transition: .spring(duration: 0.4)) - } - } else { - component.togglePanel() - } - }) - self.tabItemView = itemView - self.addSubview(itemView) - } - - let itemSize = itemView.update(context: component.context, theme: component.theme, width: 72.0, location: component.location, isReordering: self.isReordering, transition: itemTransition) - let itemFrame: CGRect - switch component.location { - case .side: - itemFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: itemSize) - directionContainerInset += itemSize.height - case .top: - itemFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: itemSize) - directionContainerInset += itemSize.width - 14.0 - } - - itemTransition.setPosition(layer: itemView.layer, position: itemFrame.center) - itemTransition.setBounds(layer: itemView.layer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) - - if animateIn && !transition.animation.isImmediate { - itemView.layer.animateAlpha(from: 0.0, to: itemView.alpha, duration: 0.15) - transition.containedViewLayoutTransition.animateTransformScale(view: itemView, from: 0.001) - } - } - - let scrollSize: CGSize - let scrollFrame: CGRect - let listContentInsets: UIEdgeInsets - switch component.location { - case .side: - scrollSize = CGSize(width: availableSize.width, height: availableSize.height - directionContainerInset) - scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: scrollSize) - listContentInsets = UIEdgeInsets(top: 8.0 + environment.insets.top, left: 0.0, bottom: 8.0 + environment.insets.bottom, right: 0.0) - case .top: - scrollSize = CGSize(width: availableSize.width - directionContainerInset, height: availableSize.height) - scrollFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: scrollSize) - listContentInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0) - } - - self.scrollContainerView.frame = scrollFrame - self.scrollViewMask.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) - - let scrollToId: ScrollId - if let threadId = component.topicId { - scrollToId = .topic(threadId) - } else { - scrollToId = .all - } - if self.appliedScrollToId != scrollToId { - self.appliedScrollToId = scrollToId - self.listState.resetScrolling(id: AnyHashable(scrollToId)) - } - - var listItems: [AnyComponentWithIdentity] = [] - switch component.location { - case .side: - listItems.append(AnyComponentWithIdentity( - id: ScrollId.all, - component: AnyComponent(VerticalAllItemComponent( - isSelected: component.topicId == nil, - kind: component.kind, - theme: component.theme, - strings: component.strings, - action: { [weak self] in - guard let self, let component = self.component else { - return - } - component.updateTopicId(nil, false) - } - ))) - ) - case .top: - listItems.append(AnyComponentWithIdentity( - id: ScrollId.all, - component: AnyComponent(HorizontalAllItemComponent( - isSelected: component.topicId == nil, - kind: component.kind, - theme: component.theme, - strings: component.strings, - action: { [weak self] in - guard let self, let component = self.component else { - return - } - component.updateTopicId(nil, false) - } - ))) - ) - } - for item in self.reorderingItems ?? self.rawItems { - let scrollId: ScrollId - let topicId: Int64 - var isItemReordering = false - switch item.item.id { - case let .chatList(peerId): - topicId = peerId.toInt64() - case let .forum(topicIdValue): - topicId = topicIdValue - if self.isReordering { - if case let .forum(pinnedIndex, _, _, _, _) = item.item.index, case .index = pinnedIndex { - isItemReordering = true - } - } - } - scrollId = .topic(topicId) - - let itemAction: (() -> Void)? = self.isReordering ? nil : { [weak self] in - guard let self, let component = self.component else { - return - } - - let direction: Bool - if let lhsIndex = self.topicIndex(threadId: component.topicId), let rhsIndex = self.topicIndex(threadId: topicId) { - direction = lhsIndex < rhsIndex - } else { - direction = false - } - component.updateTopicId(topicId, direction) - } - var itemContextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? - if !self.isReordering, case .monoforum = component.kind { - itemContextGesture = { [weak self] gesture, sourceNode in - Task { @MainActor in - guard let self, let peerId = self.peerId, let component = self.component else { - return - } - guard let controller = component.controller() else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - - if let listView = self.list.view as? AsyncListComponent.View { - listView.stopScrolling() - } - - let topicId: Int64 - switch item.item.id { - case let .chatList(peerId): - topicId = peerId.toInt64() - case let .forum(topicIdValue): - topicId = topicIdValue - } - - var items: [ContextMenuItem] = [] - - let threadInfo = await component.context.engine.data.get( - TelegramEngine.EngineData.Item.Messages.ThreadInfo(peerId: peerId, threadId: topicId) - ).get() - - if let threadInfo, threadInfo.isMessageFeeRemoved { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ReinstatePaidMessages, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - guard let self, let peerId = self.peerId, let component = self.component else { - return - } - - c?.dismiss(completion: {}) - - let _ = component.context.engine.peers.reinstateNoPaidMessagesException(scopePeerId: peerId, peerId: EnginePeer.Id(topicId)).startStandalone() - }))) - } - - if !items.isEmpty { - items.append(.separator) - } - items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in - guard let self else { - return - } - - c?.dismiss(completion: { [weak self] in - guard let self, let component = self.component else { - return - } - component.openDeletePeer(topicId) - }) - }))) - - let contextController = ContextController( - presentationData: presentationData, - source: .extracted(ItemExtractedContentSource( - sourceNode: sourceNode, - containerView: self, - keepInPlace: false - )), - items: .single(ContextController.Items(content: .list(items))), - recognizer: nil, - gesture: gesture - ) - controller.presentInGlobalOverlay(contextController) - } - } - } else if !self.isReordering { - itemContextGesture = { [weak self] gesture, sourceNode in - guard let self, let peerId = self.peerId, let component = self.component else { - return - } - guard let controller = component.controller() else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - - if let listView = self.list.view as? AsyncListComponent.View { - listView.stopScrolling() - } - - let topicId: Int64 - switch item.item.id { - case let .chatList(peerId): - topicId = peerId.toInt64() - case let .forum(topicIdValue): - topicId = topicIdValue - } - - var isPinned = false - if case let .forum(pinnedIndex, _, _, _, _) = item.item.index { - if case .index = pinnedIndex { - isPinned = true - } - } - let isClosed = item.item.threadData?.isClosed - let threadData = item.item.threadData - - let _ = (chatForumTopicMenuItems( - context: component.context, - peerId: peerId, - threadId: topicId, - isPinned: isPinned, - isClosed: isClosed, - chatListController: controller, - joined: true, - canSelect: false, - customEdit: { [weak self] contextController in - contextController.dismiss(completion: { - guard let self, let peerId = self.peerId, let component = self.component, let threadData else { - return - } - let editController = component.context.sharedContext.makeEditForumTopicScreen( - context: component.context, - peerId: peerId, - threadId: topicId, - threadInfo: threadData.info, - isHidden: threadData.isHidden - ) - component.controller()?.push(editController) - }) - }, - customPinUnpin: { [weak self] contextController in - guard let self, let peerId = self.peerId, let component = self.component else { - contextController.dismiss(completion: {}) - return - } - - self.isTogglingPinnedItem = true - self.dismissContextControllerOnNextUpdate = contextController - - let _ = (component.context.engine.peers.toggleForumChannelTopicPinned(id: peerId, threadId: topicId) - |> deliverOnMainQueue).startStandalone(error: { [weak self, weak contextController] error in - guard let self, let component = self.component else { - contextController?.dismiss(completion: {}) - return - } - - switch error { - case let .limitReached(count): - contextController?.dismiss(completion: {}) - if let controller = component.controller() { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text = presentationData.strings.ChatList_MaxThreadPinsFinalText(Int32(count)) - controller.present(textAlertController(context: component.context, title: presentationData.strings.Premium_LimitReached, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true), in: .window(.root)) - } - default: - break - } - }) - }, - reorder: { [weak self] in - guard let self else { - return - } - self.updateIsReordering(isReordering: true) - }, - onDeleted: { [weak self] in - guard let self, let component = self.component else { - return - } - component.updateTopicId(nil, false) - } - ) - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak sourceNode, weak gesture] items in - guard let self, let component = self.component else { - return - } - guard let controller = component.controller() else { - return - } - guard let sourceNode else { - return - } - - let contextController = ContextController( - presentationData: presentationData, - source: .extracted(ItemExtractedContentSource( - sourceNode: sourceNode, - containerView: self, - keepInPlace: false - )), - items: .single(ContextController.Items(content: .list(items))), - recognizer: nil, - gesture: gesture - ) - controller.presentInGlobalOverlay(contextController) - }) - } - } - - switch component.location { - case .side: - listItems.append(AnyComponentWithIdentity( - id: scrollId, - component: AnyComponent(VerticalItemComponent( - context: component.context, - item: item, - isSelected: component.topicId == topicId, - isReordering: isItemReordering, - theme: component.theme, - strings: component.strings, - action: itemAction, - contextGesture: itemContextGesture - ))) - ) - case .top: - listItems.append(AnyComponentWithIdentity( - id: scrollId, - component: AnyComponent(HorizontalItemComponent( - context: component.context, - item: item, - isSelected: component.topicId == topicId, - isReordering: isItemReordering, - theme: component.theme, - strings: component.strings, - action: itemAction, - contextGesture: itemContextGesture - ))) - ) - } - } - - let _ = self.list.update( - transition: transition, - component: AnyComponent(AsyncListComponent( - externalState: self.listState, - items: listItems, - itemSetId: AnyHashable(self.itemsContentVersion), - direction: component.location == .side ? .vertical : .horizontal, - insets: listContentInsets, - reorderItems: { [weak self] fromIndex, toIndex in - guard let self else { - return false - } - if !self.isReordering { - return false - } - - if self.reorderingItems == nil { - self.reorderingItems = self.rawItems - } - if var reorderingItems = self.reorderingItems { - var maxToIndex = -1 - for item in reorderingItems { - if case let .forum(pinnedIndex, _, _, _, _) = item.item.index, case .index = pinnedIndex { - maxToIndex += 1 - } else { - break - } - } - - let fromItemIndex = fromIndex - 1 - // Account for synthesized "all" item: [all, item_0, item_1, ...] - let toItemIndex = max(0, min(maxToIndex, toIndex - 1)) - if fromItemIndex == toItemIndex { - return false - } - - let reorderingItem = reorderingItems[fromItemIndex] - if toItemIndex < fromItemIndex { - reorderingItems.remove(at: fromItemIndex) - reorderingItems.insert(reorderingItem, at: toItemIndex) - } else { - reorderingItems.insert(reorderingItem, at: toItemIndex + 1) - reorderingItems.remove(at: fromItemIndex) - } - - self.reorderingItems = reorderingItems - self.state?.updated(transition: .spring(duration: 0.4)) - } - - return true - }, - onVisibleItemsUpdated: { [weak self] visibleItems, transition in - guard let self else { - return - } - self.updateListOverlays(visibleItems: visibleItems, transition: transition) - } - )), - environment: {}, - containerSize: scrollSize - ) - if let listView = self.list.view { - if listView.superview == nil { - self.scrollContainerView.addSubview(listView) - } - transition.setFrame(view: listView, frame: CGRect(origin: CGPoint(), size: scrollSize)) - } - - if self.isTogglingPinnedItem { - self.isTogglingPinnedItem = false - } - if let dismissContextControllerOnNextUpdate = self.dismissContextControllerOnNextUpdate { - self.dismissContextControllerOnNextUpdate = nil - dismissContextControllerOnNextUpdate.dismiss(completion: {}) - } - - return availableSize - } - } - - 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) - } -} - -private final class ItemExtractedContentSource: ContextExtractedContentSource { - let keepInPlace: Bool - let ignoreContentTouches: Bool = true - let blurBackground: Bool = true - let adjustContentForSideInset: Bool = true - - private let sourceNode: ContextExtractedContentContainingNode - private weak var containerView: UIView? - - init(sourceNode: ContextExtractedContentContainingNode, containerView: UIView, keepInPlace: Bool) { - self.sourceNode = sourceNode - self.containerView = containerView - self.keepInPlace = keepInPlace - } - - func takeView() -> ContextControllerTakeViewInfo? { - var contentArea: CGRect? - if let containerView = self.containerView { - contentArea = containerView.convert(containerView.bounds, to: nil) - } - - return ContextControllerTakeViewInfo( - containingItem: .node(self.sourceNode), - contentAreaInScreenSpace: contentArea ?? UIScreen.main.bounds - ) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) - } -}*/ diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index e8ef797c5a..0d249563a1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -1250,8 +1250,7 @@ public final class ChatSideTopicsPanel: Component { let titleText: String if case .botForum = component.kind { - //TODO:localize - titleText = "New Chat" + titleText = component.strings.Chat_InlineTopicMenu_NewForumThreadTab } else { titleText = component.strings.Chat_InlineTopicMenu_AllTab } @@ -1388,8 +1387,7 @@ public final class ChatSideTopicsPanel: Component { let titleText: String if case .botForum = component.kind { - //TODO:localize - titleText = "New Chat" + titleText = component.strings.Chat_InlineTopicMenu_NewForumThreadTab } else { titleText = component.strings.Chat_InlineTopicMenu_AllTab } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 1ed94d18ab..8e6eb21423 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -339,8 +339,7 @@ extension ChatControllerImpl { var items: [ContextMenuItem] = [] - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Open Profile", icon: { theme in + items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuOpenProfile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index 2c7a4bc10f..31d439a2f6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerContentData.swift +++ b/submodules/TelegramUI/Sources/ChatControllerContentData.swift @@ -1526,8 +1526,7 @@ extension ChatControllerImpl { var customSubtitle: String? if messageAndTopic.messageCount == 0, let peer = peerView.peers[peerView.peerId] as? TelegramUser { if peer.isForum { - //TODO:localize - customSubtitle = "topic" + customSubtitle = strongSelf.presentationData.strings.Chat_GenericForuThreadStatus } } @@ -1550,8 +1549,7 @@ extension ChatControllerImpl { } strongSelf.state.infoAvatar = .emojiStatus(content: avatarContent, contextActionIsEnabled: infoContextActionIsEnabled) } else if chatLocation.threadId == EngineMessage.newTopicThreadId { - //TODO:localize - strongSelf.state.chatTitleContent = .custom("New Chat", nil, false) + strongSelf.state.chatTitleContent = .custom(strongSelf.presentationData.strings.Chat_MessageHeaderBotNewThread, nil, false) strongSelf.state.infoAvatar = nil } else { strongSelf.state.chatTitleContent = .replyThread(type: replyThreadType, count: count)