diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f09b8368f3..53d72796a8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8142,3 +8142,5 @@ Sorry for the inconvenience."; "Channel.AdminLog.MessageChangedGroupUsernames" = "%@ changed group links:"; "Channel.AdminLog.MessageChangedChannelUsernames" = "%@ changed channel links:"; "Channel.AdminLog.MessagePreviousLinks" = "Previous links"; + +"ShareMenu.SelectTopic" = "Select topic"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 85abff2b14..1752873e8a 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2018,7 +2018,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let fileId = threadInfo.info.icon, fileId != 0 { avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever) } else { - avatarIconContent = .topic(title: String(threadInfo.info.title.prefix(1)), colorIndex: Int(clamping: abs(threadInfo.id)), size: CGSize(width: 32.0, height: 32.0)) + avatarIconContent = .topic(title: String(threadInfo.info.title.prefix(1)), color: threadInfo.info.iconColor, size: CGSize(width: 32.0, height: 32.0)) } let avatarIconComponent = EmojiStatusComponent( diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index a0e56d0e09..f1ad573767 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1053,7 +1053,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { if let fileId = icon, fileId != 0 { avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever) } else { - avatarIconContent = .topic(title: String(title.prefix(1)), colorIndex: Int(clamping: abs(color)), size: CGSize(width: 32.0, height: 32.0)) + avatarIconContent = .topic(title: String(title.prefix(1)), color: color, size: CGSize(width: 32.0, height: 32.0)) } let avatarIconComponent = EmojiStatusComponent( diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index 25b75c39bf..65ba09f228 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -21,7 +21,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone13Pro case iPhone13ProMax case iPhone14Pro + case iPhone14ProZoomed case iPhone14ProMax + case iPhone14ProMaxZoomed case iPad case iPadMini case iPad102Inch @@ -49,7 +51,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, + .iPhone14ProZoomed, .iPhone14ProMax, + .iPhone14ProMaxZoomed, .iPad, .iPadMini, .iPad102Inch, @@ -83,7 +87,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { let width = device.screenSize.width let height = device.screenSize.height if ((screenSize.width.isEqual(to: width) && screenSize.height.isEqual(to: height)) || (additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height))) { - if case .iPhoneXSMax = device, scale == 2.0 { + if case .iPhoneX = device, statusBarHeight == 47.0 { + self = .iPhone14ProMaxZoomed + } else if case .iPhoneXSMax = device, scale == 2.0 { self = .iPhoneXr } else { self = device @@ -135,8 +141,12 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 428.0, height: 926.0) case .iPhone14Pro: return CGSize(width: 393.0, height: 852.0) + case .iPhone14ProZoomed: + return CGSize(width: 320.0, height: 693.0) case .iPhone14ProMax: return CGSize(width: 430.0, height: 932.0) + case .iPhone14ProMaxZoomed: + return CGSize(width: 375.0, height: 812.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -164,9 +174,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 41.0 + UIScreenPixel case .iPhone12Mini: return 44.0 - case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro: + case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed: return 47.0 + UIScreenPixel - case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: + case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 53.0 + UIScreenPixel case let .unknown(_, _, onScreenNavigationHeight): if let _ = onScreenNavigationHeight { @@ -181,7 +191,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -192,6 +202,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: return inLandscape ? 21.0 : 34.0 + case .iPhone14ProZoomed: + return inLandscape ? 21.0 : 28.0 + case .iPhone14ProMaxZoomed: + return inLandscape ? 21.0 : 31.0 case .iPadPro3rdGen, .iPadPro11Inch: return 21.0 case .iPad, .iPadPro, .iPadPro10Inch, .iPadMini, .iPadMini6thGen: @@ -224,6 +238,8 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone14Pro, .iPhone14ProMax: return 54.0 + case .iPhone14ProMaxZoomed: + return 47.0 case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: @@ -242,7 +258,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -261,7 +277,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: return 292.0 case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: return 302.0 @@ -282,7 +298,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -293,7 +309,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return 44.0 case .iPhone6Plus: return 45.0 @@ -316,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { public var hasDynamicIsland: Bool { switch self { - case .iPhone14Pro, .iPhone14ProMax: + case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed: return true default: return false diff --git a/submodules/Display/Source/NativeWindowHostView.swift b/submodules/Display/Source/NativeWindowHostView.swift index 7db299b632..95e15f0569 100644 --- a/submodules/Display/Source/NativeWindowHostView.swift +++ b/submodules/Display/Source/NativeWindowHostView.swift @@ -85,12 +85,20 @@ private final class WindowRootViewController: UIViewController, UIViewController didSet { if oldValue != self.orientations { if self.orientations == .portrait { - if UIDevice.current.orientation != .portrait { + if #available(iOSApplicationExtension 16.0, iOS 16.0, *) { + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) + self.setNeedsUpdateOfSupportedInterfaceOrientations() + } else if UIDevice.current.orientation != .portrait { let value = UIInterfaceOrientation.portrait.rawValue UIDevice.current.setValue(value, forKey: "orientation") } } else { - UIViewController.attemptRotationToDeviceOrientation() + if #available(iOSApplicationExtension 16.0, iOS 16.0, *) { + self.setNeedsUpdateOfSupportedInterfaceOrientations() + } else { + UIViewController.attemptRotationToDeviceOrientation() + } } } } diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index af90c5875a..bb5da5be73 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -160,7 +160,7 @@ open class NavigationController: UINavigationController, ContainableController, public private(set) var validLayout: ContainerViewLayout? private var validStatusBarStyle: NavigationStatusBarStyle? - private var validStatusBarHidden: Bool = false + private var validStatusBarHidden: Bool? private var ignoreInputHeight: Bool = false private var currentStatusBarExternalHidden: Bool = false diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index c671f9d62e..4a3de05c54 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -270,7 +270,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.contentOffsetUpdated?(-size.height + nodeHeight, transition) } - func updateSelectedPeers() { + func updateSelectedPeers(animated: Bool) { } } @@ -318,6 +318,6 @@ public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareCont self.contentOffsetUpdated?(-size.height + nodeHeight, transition) } - public func updateSelectedPeers() { + public func updateSelectedPeers(animated: Bool) { } } diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift index beabe36a7a..73556a8b07 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift @@ -110,6 +110,6 @@ final class LanguageLinkPreviewContentNode: ASDisplayNode, ShareContentContainer self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) } - func updateSelectedPeers() { + func updateSelectedPeers(animated: Bool) { } } diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 19efb88a84..5819862754 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -151,7 +151,7 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 diff --git a/submodules/SelectablePeerNode/BUILD b/submodules/SelectablePeerNode/BUILD index 3ab6648992..39b96b1280 100644 --- a/submodules/SelectablePeerNode/BUILD +++ b/submodules/SelectablePeerNode/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/LocalizedPeerData:LocalizedPeerData", "//submodules/AccountContext:AccountContext", "//submodules/CheckNode:CheckNode", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index df9276e2ba..17164ea1d2 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -12,6 +12,8 @@ import ContextUI import LocalizedPeerData import AccountContext import CheckNode +import ComponentFlow +import EmojiStatusComponent private let avatarFont = avatarPlaceholderFont(size: 24.0) private let textFont = Font.regular(11.0) @@ -72,6 +74,8 @@ public final class SelectablePeerNode: ASDisplayNode { private let onlineNode: PeerOnlineMarkerNode private var checkNode: CheckNode? private let textNode: ASTextNode + + private let iconView: ComponentView public var toggleSelection: (() -> Void)? public var contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? { @@ -119,6 +123,8 @@ public final class SelectablePeerNode: ASDisplayNode { self.onlineNode = PeerOnlineMarkerNode() + self.iconView = ComponentView() + super.init() self.addSubnode(self.contextContainer) @@ -137,7 +143,7 @@ public final class SelectablePeerNode: ASDisplayNode { } } - public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { + public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { let isFirstTime = self.peer == nil self.peer = peer guard let mainPeer = peer.chatMainPeer else { @@ -166,7 +172,7 @@ public final class SelectablePeerNode: ASDisplayNode { } } self.textNode.maximumNumberOfLines = numberOfLines - self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: customTitle ?? text, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : defaultColor, paragraphAlignment: .center) self.avatarNode.setPeer(context: context, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: isForum ? .roundedRect : .round, synchronousLoad: synchronousLoad) let onlineLayout = self.onlineNode.asyncLayout() @@ -176,6 +182,40 @@ public final class SelectablePeerNode: ASDisplayNode { self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel), color: nil, transition: .immediate) self.onlineNode.frame = CGRect(origin: CGPoint(), size: onlineSize) + let iconContent: EmojiStatusComponent.Content? + if let fileId = iconId { + iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 18.0, height: 18.0), placeholderColor: theme.actionSheet.disabledActionTextColor, themeColor: theme.actionSheet.primaryTextColor, loopMode: .count(2)) + } else if let customTitle = customTitle { + iconContent = .topic(title: String(customTitle.prefix(1)), color: iconColor ?? 0, size: CGSize(width: 18.0, height: 18.0)) + } else { + iconContent = nil + } + + if let iconContent = iconContent { + let iconSize = self.iconView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent(EmojiStatusComponent( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + content: iconContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 18.0, height: 18.0) + ) + + if let iconComponentView = self.iconView.view { + if iconComponentView.superview == nil { + self.view.addSubview(iconComponentView) + } + iconComponentView.frame = CGRect(origin: .zero, size: iconSize) + } + } else if let iconComponentView = self.iconView.view { + iconComponentView.removeFromSuperview() + } + self.setNeedsLayout() } @@ -267,7 +307,18 @@ public final class SelectablePeerNode: ASDisplayNode { self.contextContainer.frame = bounds self.avatarNodeContainer.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) - self.textNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 4.0 + 60.0 + 4.0), size: CGSize(width: bounds.size.width - 4.0, height: 34.0)) + + let iconSize = CGSize(width: 18.0, height: 18.0) + let textSize = self.textNode.calculateSizeThatFits(bounds.size) + var totalWidth = textSize.width + var leftOrigin = floorToScreenPixels((bounds.width - textSize.width) / 2.0) + if let iconView = self.iconView.view, iconView.superview != nil { + totalWidth += iconView.frame.width + 2.0 + leftOrigin = floorToScreenPixels((bounds.width - totalWidth) / 2.0) + iconView.frame = CGRect(origin: CGPoint(x: leftOrigin, y: 4.0 + 60.0 + 1.0), size: iconSize) + leftOrigin += iconSize.width + 2.0 + } + self.textNode.frame = CGRect(origin: CGPoint(x: leftOrigin, y: 4.0 + 60.0 + 4.0), size: CGSize(width: textSize.width, height: 34.0)) let avatarFrame = self.avatarNode.frame let avatarContainerFrame = self.avatarNodeContainer.frame diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 7ff8806c79..2a7672158e 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -35,6 +35,8 @@ swift_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareContentContainerNode.swift b/submodules/ShareController/Sources/ShareContentContainerNode.swift index 7f815e9b84..b28cf023b4 100644 --- a/submodules/ShareController/Sources/ShareContentContainerNode.swift +++ b/submodules/ShareController/Sources/ShareContentContainerNode.swift @@ -10,5 +10,5 @@ public protocol ShareContentContainerNode: AnyObject { func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) - func updateSelectedPeers() + func updateSelectedPeers(animated: Bool) } diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 0eebfd0697..276b5e9ac9 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1011,12 +1011,14 @@ public final class ShareController: ViewController { return (resultPeers, accountPeer) } }) + var animatedIn = false self.peersDisposable.set((self.peers.get() |> deliverOnMainQueue).start(next: { [weak self] next in if let strongSelf = self { strongSelf.controllerNode.updatePeers(context: strongSelf.sharedContext.makeTempAccountContext(account: strongSelf.currentAccount), switchableAccounts: strongSelf.switchableAccounts, peers: next.0, accountPeer: next.1, defaultAction: strongSelf.defaultAction) - if animateIn { + if animateIn && !animatedIn { + animatedIn = true strongSelf.readyDisposable.set((strongSelf.controllerNode.ready.get() |> filter({ $0 }) |> take(1) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index aa0e4a5c34..a3691edfc6 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -73,6 +73,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private var controllerInteraction: ShareControllerInteraction? private var peersContentNode: SharePeersContainerNode? + private var topicsContentNode: ShareTopicsContainerNode? private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? @@ -244,15 +245,28 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer, search in if let strongSelf = self { var added = false + var openedTopic = false if strongSelf.controllerInteraction!.selectedPeerIds.contains(peer.peerId) { + strongSelf.controllerInteraction!.selectedTopics[peer.peerId] = nil + strongSelf.peersContentNode?.update() strongSelf.controllerInteraction!.selectedPeerIds.remove(peer.peerId) strongSelf.controllerInteraction!.selectedPeers = strongSelf.controllerInteraction!.selectedPeers.filter({ $0.peerId != peer.peerId }) } else { - strongSelf.controllerInteraction!.selectedPeerIds.insert(peer.peerId) - strongSelf.controllerInteraction!.selectedPeers.append(peer) + if case let .channel(channel) = peer.peer, channel.flags.contains(.isForum) { + if strongSelf.controllerInteraction!.selectedTopics[peer.peerId] != nil { + strongSelf.controllerInteraction!.selectedTopics[peer.peerId] = nil + strongSelf.peersContentNode?.update() + } else { + strongSelf.transitionToPeerTopics(peer) + openedTopic = true + } + } else { + strongSelf.controllerInteraction!.selectedPeerIds.insert(peer.peerId) + strongSelf.controllerInteraction!.selectedPeers.append(peer) + added = true + } strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.peerId) - added = true } if search && added { @@ -263,12 +277,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.peersContentNode?.updateFoundPeers() } - strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil) - - strongSelf.updateButton() - - strongSelf.peersContentNode?.updateSelectedPeers() - strongSelf.contentNode?.updateSelectedPeers() + if !openedTopic { + strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil) + + strongSelf.updateButton() + + strongSelf.peersContentNode?.updateSelectedPeers(animated: true) + strongSelf.contentNode?.updateSelectedPeers(animated: true) + } if let (layout, navigationBarHeight, _) = strongSelf.containerLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) @@ -280,6 +296,17 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } } + }, selectTopic: { [weak self] peer, threadId, threadData in + if let strongSelf = self { + strongSelf.controllerInteraction?.selectedPeerIds.insert(peer) + strongSelf.controllerInteraction?.selectedTopics[peer] = (threadId, threadData) + strongSelf.peersContentNode?.update() + strongSelf.peersContentNode?.updateSelectedPeers(animated: false) + strongSelf.updateButton() + Queue.mainQueue().after(0.01, { + strongSelf.closePeerTopics(peer) + }) + } }) self.backgroundColor = nil @@ -324,7 +351,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate deinit { self.shareDisposable.dispose() } - + override func didLoad() { super.didLoad() @@ -333,6 +360,70 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + func transitionToPeerTopics(_ peer: EngineRenderedPeer) { + guard let context = self.context, let mainPeer = peer.chatMainPeer else { + return + } + + let _ = (threadList(context: context, peerId: mainPeer.id) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] threads in + guard let strongSelf = self, let context = strongSelf.context, let controllerInteraction = strongSelf.controllerInteraction else { + return + } + + let topicsContentNode = ShareTopicsContainerNode( + sharedContext: strongSelf.sharedContext, + context: context, + theme: strongSelf.presentationData.theme, + strings: strongSelf.presentationData.strings, + peer: mainPeer, + topics: threads.items.reversed(), + controllerInteraction: controllerInteraction + ) + topicsContentNode.backPressed = { [weak self] in + if let strongSelf = self { + strongSelf.closePeerTopics(peer.peerId) + } + } + strongSelf.topicsContentNode = topicsContentNode + strongSelf.contentNode?.supernode?.addSubnode(topicsContentNode) + topicsContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + + if let (layout, navigationBarHeight, _) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + + if let sourceFrame = strongSelf.peersContentNode?.animateOut(peerId: peer.peerId) { + topicsContentNode.animateIn(sourceFrame: sourceFrame) + } + }) + } + + func closePeerTopics(_ peerId: EnginePeer.Id) { + if let topicsContentNode = self.topicsContentNode, let peersContentNode = self.peersContentNode { + topicsContentNode.supernode?.insertSubnode(topicsContentNode, belowSubnode: peersContentNode) + } + self.peersContentNode?.setContentOffsetUpdated({ [weak self] contentOffset, transition in + self?.contentNodeOffsetUpdated(contentOffset, transition: transition) + }) + + if let (layout, navigationBarHeight, _) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + + if let targetFrame = self.peersContentNode?.animateIn(peerId: peerId), let topicsContentNode = self.topicsContentNode { + topicsContentNode.animateOut(targetFrame: targetFrame, completion: { [weak self] in + if let topicsContentNode = self?.topicsContentNode { + topicsContentNode.removeFromSupernode() + self?.topicsContentNode = nil + } + }) + } + } + func updatePresentationData(_ presentationData: PresentationData) { guard self.presentationData !== presentationData else { return @@ -540,6 +631,11 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) contentNode.updateLayout(size: gridSize, isLandscape: layout.size.width > layout.size.height, bottomInset: bottomGridInset, transition: transition) } + + if let topicsContentNode = self.topicsContentNode { + transition.updateFrame(node: topicsContentNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) + topicsContentNode.updateLayout(size: gridSize, isLandscape: layout.size.width > layout.size.height, bottomInset: bottomGridInset, transition: transition) + } } private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { @@ -967,7 +1063,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } private func updateButton() { - if self.controllerInteraction!.selectedPeers.isEmpty { + let count = self.controllerInteraction!.selectedPeerIds.count + if count == 0 { if self.presetText != nil { self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal) self.actionButtonNode.isEnabled = false @@ -990,13 +1087,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let text: String if let segmentedValues = self.segmentedValues { let value = segmentedValues[self.selectedSegmentedIndex] - text = value.formatSendTitle(self.controllerInteraction!.selectedPeers.count) + text = value.formatSendTitle(count) } else { text = self.presentationData.strings.ShareMenu_Send } self.actionButtonNode.isEnabled = true self.actionButtonNode.setTitle(text, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) - self.actionButtonNode.badge = "\(self.controllerInteraction!.selectedPeers.count)" + self.actionButtonNode.badge = "\(count)" } } @@ -1098,3 +1195,63 @@ private final class ShareContextReferenceContentSource: ContextReferenceContentS return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, customPosition: self.customPosition) } } + +private func threadList(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + let viewKey: PostboxViewKey = .messageHistoryThreadIndex( + id: peerId, + summaryComponents: ChatListEntrySummaryComponents( + components: [:] + ) + ) + + return context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> EngineChatList in + guard let view = views.views[viewKey] as? MessageHistoryThreadIndexView else { + preconditionFailure() + } + + var items: [EngineChatList.Item] = [] + for item in view.items { + guard let peer = view.peer else { + continue + } + guard let data = item.info.get(MessageHistoryThreadData.self) else { + continue + } + + let pinnedIndex: EngineChatList.Item.PinnedIndex + if let index = item.pinnedIndex { + pinnedIndex = .index(index) + } else { + pinnedIndex = .none + } + + items.append(EngineChatList.Item( + id: .forum(item.id), + index: .forum(pinnedIndex: pinnedIndex, timestamp: item.index.timestamp, threadId: item.id, namespace: item.index.id.namespace, id: item.index.id.id), + messages: item.topMessage.flatMap { [EngineMessage($0)] } ?? [], + readCounters: EnginePeerReadCounters(state: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, .idBased(maxIncomingReadId: 1, maxOutgoingReadId: 1, maxKnownId: 1, count: data.incomingUnreadCount, markedUnread: false))])), + isMuted: false, + draft: nil, + threadData: data, + renderedPeer: EngineRenderedPeer(peer: EnginePeer(peer)), + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + forumTopicData: nil, + hasFailed: false, + isContact: false + )) + } + + let list = EngineChatList( + items: items.reversed(), + groupItems: [], + additionalItems: [], + hasEarlier: false, + hasLater: false, + isLoading: view.isLoading + ) + return list + } +} diff --git a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift index fe273dfda5..f8c839ccbd 100644 --- a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift @@ -16,10 +16,15 @@ final class ShareControllerInteraction { var foundPeers: [EngineRenderedPeer] = [] var selectedPeerIds = Set() var selectedPeers: [EngineRenderedPeer] = [] - let togglePeer: (EngineRenderedPeer, Bool) -> Void - init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void) { + var selectedTopics: [EnginePeer.Id: (Int64, MessageHistoryThreadData)] = [:] + + let togglePeer: (EngineRenderedPeer, Bool) -> Void + let selectTopic: (EnginePeer.Id, Int64, MessageHistoryThreadData) -> Void + + init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void, selectTopic: @escaping (EnginePeer.Id, Int64, MessageHistoryThreadData) -> Void) { self.togglePeer = togglePeer + self.selectTopic = selectTopic } } @@ -92,17 +97,21 @@ final class ShareControllerPeerGridItem: GridItem { let strings: PresentationStrings let peer: EngineRenderedPeer? let presence: EnginePeer.Presence? + let topicId: Int64? + let threadData: MessageHistoryThreadData? let controllerInteraction: ShareControllerInteraction let search: Bool let section: GridSection? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, presence: EnginePeer.Presence?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) { self.context = context self.theme = theme self.strings = strings self.peer = peer self.presence = presence + self.topicId = topicId + self.threadData = threadData self.controllerInteraction = controllerInteraction self.search = search @@ -116,7 +125,7 @@ final class ShareControllerPeerGridItem: GridItem { func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = ShareControllerPeerGridItemNode() node.controllerInteraction = self.controllerInteraction - node.setup(context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, search: self.search, synchronousLoad: synchronousLoad, force: false) + node.setup(context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: self.topicId, threadData: self.threadData, search: self.search, synchronousLoad: synchronousLoad, force: false) return node } @@ -126,12 +135,12 @@ final class ShareControllerPeerGridItem: GridItem { return } node.controllerInteraction = self.controllerInteraction - node.setup(context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, search: self.search, synchronousLoad: false, force: false) + node.setup(context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: self.topicId, threadData: self.threadData, search: self.search, synchronousLoad: false, force: false) } } final class ShareControllerPeerGridItemNode: GridItemNode { - private var currentState: (AccountContext, PresentationTheme, PresentationStrings, EngineRenderedPeer?, Bool, EnginePeer.Presence?)? + private var currentState: (AccountContext, PresentationTheme, PresentationStrings, EngineRenderedPeer?, Bool, EnginePeer.Presence?, Int64?, MessageHistoryThreadData?)? private let peerNode: SelectablePeerNode private var presenceManager: PeerPresenceStatusManager? @@ -140,6 +149,10 @@ final class ShareControllerPeerGridItemNode: GridItemNode { private var placeholderNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? + var peerId: EnginePeer.Id? { + return self.currentState?.3?.peerId + } + override init() { self.peerNode = SelectablePeerNode() @@ -147,7 +160,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.peerNode.toggleSelection = { [weak self] in if let strongSelf = self { - if let (_, _, _, maybePeer, search, _) = strongSelf.currentState, let peer = maybePeer { + if let (_, _, _, maybePeer, search, _, _, _) = strongSelf.currentState, let peer = maybePeer { if let _ = peer.peers[peer.peerId] { strongSelf.controllerInteraction?.togglePeer(peer, search) } @@ -159,7 +172,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { guard let strongSelf = self, let currentState = strongSelf.currentState else { return } - strongSelf.setup(context: currentState.0, theme: currentState.1, strings: currentState.2, peer: currentState.3, presence: currentState.5, search: currentState.4, synchronousLoad: false, force: true) + strongSelf.setup(context: currentState.0, theme: currentState.1, strings: currentState.2, peer: currentState.3, presence: currentState.5, topicId: currentState.6, threadData: currentState.7, search: currentState.4, synchronousLoad: false, force: true) }) } @@ -171,8 +184,8 @@ final class ShareControllerPeerGridItemNode: GridItemNode { } } - func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, presence: EnginePeer.Presence?, search: Bool, synchronousLoad: Bool, force: Bool) { - if force || self.currentState == nil || self.currentState!.0 !== context || self.currentState!.3 != peer || self.currentState!.5 != presence { + func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, search: Bool, synchronousLoad: Bool, force: Bool) { + if force || self.currentState == nil || self.currentState!.0 !== context || self.currentState!.3 != peer || self.currentState!.5 != presence || self.currentState!.6 != topicId { let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: theme.chatList.secretTitleColor, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.checkContentColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -186,7 +199,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.peerNode.theme = itemTheme if let peer = peer { - self.peerNode.setup(context: context, theme: theme, strings: strings, peer: peer, online: online, synchronousLoad: synchronousLoad) + self.peerNode.setup(context: context, theme: theme, strings: strings, peer: peer, customTitle: threadData?.info.title, iconId: threadData?.info.icon, iconColor: threadData?.info.iconColor ?? 0, online: online, synchronousLoad: synchronousLoad) if let shimmerNode = self.placeholderNode { self.placeholderNode = nil shimmerNode.removeFromSupernode() @@ -218,7 +231,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, horizontal: true, size: self.bounds.size) } - self.currentState = (context, theme, strings, peer, search, presence) + self.currentState = (context, theme, strings, peer, search, presence, topicId, threadData) self.setNeedsLayout() if let presence = presence { self.presenceManager?.reset(presence: presence) @@ -229,7 +242,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { func updateSelection(animated: Bool) { var selected = false - if let controllerInteraction = self.controllerInteraction, let (_, _, _, maybePeer, _, _) = self.currentState, let peer = maybePeer { + if let controllerInteraction = self.controllerInteraction, let (_, _, _, maybePeer, _, _, _, _) = self.currentState, let peer = maybePeer { selected = controllerInteraction.selectedPeerIds.contains(peer.peerId) } @@ -243,7 +256,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.peerNode.frame = bounds self.placeholderNode?.frame = bounds - if let (_, theme, _, _, _, _) = self.currentState, let shimmerNode = self.placeholderNode { + if let (_, theme, _, _, _, _, _, _) = self.currentState, let shimmerNode = self.placeholderNode { var shapes: [ShimmerEffectNode.Shape] = [] let titleLineWidth: CGFloat = 56.0 diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 710aa377cb..7c9589592b 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -91,7 +91,7 @@ public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContain self.contentOffsetUpdated?(-size.height + 64.0, transition) } - public func updateSelectedPeers() { + public func updateSelectedPeers(animated: Bool) { } } @@ -330,6 +330,6 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte self.contentOffsetUpdated?(-size.height + nodeHeight * 0.5, transition) } - public func updateSelectedPeers() { + public func updateSelectedPeers(animated: Bool) { } } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index aa1c8595a8..a3bbb7816b 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -17,10 +17,31 @@ import ContextUI private let subtitleFont = Font.regular(12.0) +private extension CGPoint { + func angle(to other: CGPoint) -> CGFloat { + let originX = other.x - self.x + let originY = other.y - self.y + let bearingRadians = atan2f(Float(originY), Float(originX)) + return CGFloat(bearingRadians) + } + + func distance(to other: CGPoint) -> CGFloat { + return sqrt((self.x - other.x) * (self.x - other.x) + (self.y - other.y) * (self.y - other.y)) + } + + func offsetBy(distance: CGFloat, inDirection radians: CGFloat) -> CGPoint { + let vertical = sin(radians) * distance + let horizontal = cos(radians) * distance + return self.offsetBy(dx: horizontal, dy: vertical) + } +} + private struct SharePeerEntry: Comparable, Identifiable { let index: Int32 let peer: EngineRenderedPeer let presence: EnginePeer.Presence? + let threadId: Int64? + let threadData: MessageHistoryThreadData? let theme: PresentationTheme let strings: PresentationStrings @@ -38,6 +59,12 @@ private struct SharePeerEntry: Comparable, Identifiable { if lhs.presence != rhs.presence { return false } + if lhs.threadId != rhs.threadId { + return false + } + if lhs.threadData != rhs.threadData { + return false + } return true } @@ -47,7 +74,7 @@ private struct SharePeerEntry: Comparable, Identifiable { } func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { - return ShareControllerPeerGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, controllerInteraction: interfaceInteraction, search: false) + return ShareControllerPeerGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: self.threadId, threadData: self.threadData, controllerInteraction: interfaceInteraction, search: false) } } @@ -114,6 +141,13 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { let peersValue = Promise<[(EngineRenderedPeer, EnginePeer.Presence?)]>() + private var _tick: Int = 0 { + didSet { + self.tick.set(self._tick) + } + } + private let tick = ValuePromise(0) + init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(EngineRenderedPeer, EnginePeer.Presence?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?) { self.sharedContext = sharedContext self.context = context @@ -129,19 +163,19 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.peersValue.set(.single(peers)) - let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get()) - |> map { initialPeers, foundPeers -> [SharePeerEntry] in + let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get(), self.tick.get()) + |> map { [weak controllerInteraction] initialPeers, foundPeers, _ -> [SharePeerEntry] in var entries: [SharePeerEntry] = [] var index: Int32 = 0 var existingPeerIds: Set = Set() - entries.append(SharePeerEntry(index: index, peer: EngineRenderedPeer(peer: accountPeer), presence: nil, theme: theme, strings: strings)) + entries.append(SharePeerEntry(index: index, peer: EngineRenderedPeer(peer: accountPeer), presence: nil, threadId: nil, threadData: nil, theme: theme, strings: strings)) existingPeerIds.insert(accountPeer.id) index += 1 for peer in foundPeers.reversed() { if !existingPeerIds.contains(peer.peerId) { - entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings)) + entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, threadId: nil, threadData: nil, theme: theme, strings: strings)) existingPeerIds.insert(peer.peerId) index += 1 } @@ -149,7 +183,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { for (peer, presence) in initialPeers { if !existingPeerIds.contains(peer.peerId) { - entries.append(SharePeerEntry(index: index, peer: peer, presence: presence, theme: theme, strings: strings)) + let thread = controllerInteraction?.selectedTopics[peer.peerId] + entries.append(SharePeerEntry(index: index, peer: peer, presence: presence, threadId: thread?.0, threadData: thread?.1, theme: theme, strings: strings)) existingPeerIds.insert(peer.peerId) index += 1 } @@ -325,6 +360,154 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { func deactivate() { } + func frameForPeerId(_ peerId: EnginePeer.Id) -> CGRect? { + var node: ASDisplayNode? + self.contentGridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { + node = itemNode + } + } + return node?.frame + } + + func generateMaskImage() -> UIImage? { + return generateImage(CGSize(width: 100.0, height: 100.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.setShadow(offset: .zero, blur: 40.0, color: UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size).insetBy(dx: 16.0, dy: 16.0)) + })?.stretchableImage(withLeftCapWidth: 49, topCapHeight: 49) + } + + func animateIn(peerId: EnginePeer.Id) -> CGRect? { + self.searchButtonNode.alpha = 1.0 + self.searchButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.searchButtonNode.layer.animatePosition(from: CGPoint(x: -20.0, y: 0.0), to: .zero, duration: 0.2, additive: true) + + self.contentTitleNode.alpha = 1.0 + self.contentTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.contentTitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -10.0), to: .zero, duration: 0.2, additive: true) + self.contentTitleNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.2) + + self.contentSubtitleNode.alpha = 1.0 + self.contentSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.contentSubtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -10.0), to: .zero, duration: 0.2, additive: true) + self.contentSubtitleNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.2) + + if let targetFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout { + let sourceCenter = targetFrame.center + let clippedNode = ASDisplayNode() + clippedNode.clipsToBounds = true + clippedNode.cornerRadius = 16.0 + clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.contentTitleNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset)) + self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view) + + let maskView = UIView() + maskView.frame = clippedNode.bounds + + let maskImageView = UIImageView() + maskImageView.image = generateMaskImage() + maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0) + maskView.addSubview(maskImageView) + + clippedNode.view.mask = maskView + + self.contentGridNode.alpha = 1.0 + self.contentGridNode.forEachItemNode { itemNode in + if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view) + + if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false) + itemNode.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak clippedNode] _ in + clippedNode?.view.removeFromSuperview() + }) + } else { + clippedNode.view.addSubview(snapshotView) + + itemNode.alpha = 0.0 + let angle = sourceCenter.angle(to: itemNode.position) + let distance = sourceCenter.distance(to: itemNode.position) + let newDistance = distance * 2.8 + let newPosition = snapshotView.center.offsetBy(distance: newDistance, inDirection: angle) + snapshotView.layer.animatePosition(from: newPosition, to: snapshotView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + snapshotView.layer.animateScale(from: 1.35, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak itemNode] _ in + itemNode?.alpha = 1.0 + }) + snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false) + } + } + } + + return targetFrame + } else { + return nil + } + } + + func animateOut(peerId: EnginePeer.Id) -> CGRect? { + self.searchButtonNode.alpha = 0.0 + self.searchButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.searchButtonNode.layer.animatePosition(from: .zero, to: CGPoint(x: -20.0, y: 0.0), duration: 0.2, additive: true) + + self.contentTitleNode.alpha = 0.0 + self.contentTitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.contentTitleNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, additive: true) + self.contentTitleNode.layer.animateScale(from: 1.0, to: 0.85, duration: 0.3) + + self.contentSubtitleNode.alpha = 0.0 + self.contentSubtitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.contentSubtitleNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, additive: true) + self.contentSubtitleNode.layer.animateScale(from: 1.0, to: 0.85, duration: 0.3) + + if let sourceFrame = self.frameForPeerId(peerId), let (size, bottomInset) = self.validLayout { + let sourceCenter = sourceFrame.center + let clippedNode = ASDisplayNode() + clippedNode.clipsToBounds = true + clippedNode.cornerRadius = 16.0 + clippedNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.contentTitleNode.frame.minY - 15.0), size: CGSize(width: size.width, height: size.height - bottomInset)) + self.contentGridNode.view.superview?.insertSubview(clippedNode.view, aboveSubview: self.contentGridNode.view) + + let maskView = UIView() + maskView.frame = clippedNode.bounds + + let maskImageView = UIImageView() + maskImageView.image = generateMaskImage() + maskImageView.frame = maskView.bounds.offsetBy(dx: 0.0, dy: 36.0) + maskView.addSubview(maskImageView) + + clippedNode.view.mask = maskView + + self.contentGridNode.forEachItemNode { itemNode in + if let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = itemNode.view.convert(itemNode.bounds, to: clippedNode.view) + clippedNode.view.addSubview(snapshotView) + + if let itemNode = itemNode as? ShareControllerPeerGridItemNode, itemNode.peerId == peerId { + + } else { + let angle = sourceCenter.angle(to: itemNode.position) + let distance = sourceCenter.distance(to: itemNode.position) + let newDistance = distance * 2.8 + let newPosition = snapshotView.center.offsetBy(distance: newDistance, inDirection: angle) + snapshotView.layer.animatePosition(from: snapshotView.center, to: newPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + snapshotView.layer.animateScale(from: 1.0, to: 1.35, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + clippedNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak clippedNode] _ in + clippedNode?.view.removeFromSuperview() + }) + + self.contentGridNode.alpha = 0.0 + + return sourceFrame + } else { + return nil + } + } + func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { let firstLayout = self.validLayout == nil self.validLayout = (size, bottomInset) @@ -348,7 +531,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } } - let gridSize = CGSize(width: size.width - 12.0, height: size.height) + let gridSize = CGSize(width: size.width - 10.0, height: size.height) self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 0.0, bottom: bottomInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: gridLayoutTransition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) gridLayoutTransition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 0.0), size: gridSize)) @@ -424,7 +607,11 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.foundPeers.set(.single(self.controllerInteraction.foundPeers)) } - func updateSelectedPeers() { + func update() { + self._tick += 1 + } + + func updateSelectedPeers(animated: Bool = true) { if self.segmentedValues != nil { self.contentTitleNode.isHidden = true self.contentSubtitleNode.isHidden = true @@ -453,7 +640,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode { - itemNode.updateSelection(animated: true) + itemNode.updateSelection(animated: animated) } } } diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 843274ac68..a2a26149f2 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -94,7 +94,7 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable { peers[associatedPeer.id] = associatedPeer } let peer = EngineRenderedPeer(RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers), associatedMedia: [:])) - return ShareControllerPeerGridItem(context: context, theme: theme, strings: strings, peer: peer, presence: presence, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) + return ShareControllerPeerGridItem(context: context, theme: theme, strings: strings, peer: peer, presence: presence, topicId: nil, threadData: nil, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true) } } } @@ -129,7 +129,7 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable { } func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { - return ShareControllerPeerGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, controllerInteraction: interfaceInteraction, search: true) + return ShareControllerPeerGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: nil, threadData: nil, controllerInteraction: interfaceInteraction, search: true) } } @@ -546,7 +546,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { func animateIn() { } - func updateSelectedPeers() { + func updateSelectedPeers(animated: Bool) { self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode { itemNode.updateSelection(animated: true) diff --git a/submodules/ShareController/Sources/ShareTopicGridItem.swift b/submodules/ShareController/Sources/ShareTopicGridItem.swift new file mode 100644 index 0000000000..34912f2118 --- /dev/null +++ b/submodules/ShareController/Sources/ShareTopicGridItem.swift @@ -0,0 +1,138 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox +import TelegramPresentationData +import TelegramStringFormatting +import SelectablePeerNode +import PeerPresenceStatusManager +import AccountContext +import ShimmerEffect +import ComponentFlow +import EmojiStatusComponent + +final class ShareTopicGridItem: GridItem { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EngineRenderedPeer? + let id: Int64 + let threadInfo: MessageHistoryThreadData + let controllerInteraction: ShareControllerInteraction + + let section: GridSection? + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, id: Int64, threadInfo: MessageHistoryThreadData, controllerInteraction: ShareControllerInteraction) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.id = id + self.threadInfo = threadInfo + self.controllerInteraction = controllerInteraction + + self.section = nil + } + + func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + return ShareTopicGridItemNode() + } + + func update(node: GridItemNode) { + + } +} + +final class ShareTopicGridItemNode: GridItemNode { + private var currentState: (AccountContext, PresentationTheme, PresentationStrings, EngineRenderedPeer?, MessageHistoryThreadData?)? + + private let iconView: ComponentView + private let textNode: ImmediateTextNode + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + + private var currentItem: ShareTopicGridItem? + var id: Int64? { + return self.currentItem?.id + } + + override init() { + self.iconView = ComponentView() + self.textNode = ImmediateTextNode() + + super.init() + + self.addSubnode(self.textNode) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) + } + + @objc private func tapped() { + if let item = self.currentItem, let peerId = item.peer?.peerId { + item.controllerInteraction.selectTopic(peerId, item.id, item.threadInfo) + } + } + + override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + let rect = absoluteRect + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + + override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { + super.updateLayout(item: item, size: size, isVisible: isVisible, synchronousLoads: synchronousLoads) + + guard let item = item as? ShareTopicGridItem else { + return + } + self.currentItem = item + + self.textNode.attributedText = NSAttributedString(string: item.threadInfo.info.title, font: Font.regular(11.0), textColor: item.theme.actionSheet.primaryTextColor) + let textSize = self.textNode.updateLayout(size) + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: 4.0 + 60.0 + 4.0), size: textSize) + self.textNode.frame = textFrame + + let iconContent: EmojiStatusComponent.Content + if let fileId = item.threadInfo.info.icon { + iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 54.0, height: 54.0), placeholderColor: item.theme.actionSheet.disabledActionTextColor, themeColor: item.theme.actionSheet.primaryTextColor, loopMode: .count(2)) + } else { + iconContent = .topic(title: String(item.threadInfo.info.title.prefix(1)), color: item.threadInfo.info.iconColor, size: CGSize(width: 64.0, height: 64.0)) + } + + let iconSize = self.iconView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent(EmojiStatusComponent( + context: item.context, + animationCache: item.context.animationCache, + animationRenderer: item.context.animationRenderer, + content: iconContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 54.0, height: 54.0) + ) + + if let iconComponentView = self.iconView.view { + if iconComponentView.superview == nil { + self.view.addSubview(iconComponentView) + } + iconComponentView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 7.0), size: iconSize) + } + } + + override func layout() { + super.layout() + + } +} diff --git a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift new file mode 100644 index 0000000000..f86b63edbe --- /dev/null +++ b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift @@ -0,0 +1,432 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import TelegramPresentationData +import TelegramUIPreferences +import MergeLists +import AvatarNode +import AccountContext +import PeerPresenceStatusManager +import AppBundle +import SegmentedControlNode +import ContextUI + +private let subtitleFont = Font.regular(12.0) + +private struct ShareTopicEntry: Comparable, Identifiable { + let index: Int32 + let peer: EngineRenderedPeer + let id: Int64 + let threadData: MessageHistoryThreadData + let theme: PresentationTheme + let strings: PresentationStrings + + var stableId: Int64 { + return self.id + } + + static func ==(lhs: ShareTopicEntry, rhs: ShareTopicEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.id != rhs.id { + return false + } + if lhs.threadData != rhs.threadData { + return false + } + + return true + } + + static func <(lhs: ShareTopicEntry, rhs: ShareTopicEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { + return ShareTopicGridItem(context: context, theme: self.theme, strings: self.strings, peer: self.peer, id: self.id, threadInfo: self.threadData, controllerInteraction: interfaceInteraction) + } +} + +private struct ShareGridTransaction { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + let animated: Bool +} + +private func preparedGridEntryTransition(context: AccountContext, from fromEntries: [ShareTopicEntry], to toEntries: [ShareTopicEntry], interfaceInteraction: ShareControllerInteraction) -> ShareGridTransaction { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interfaceInteraction: interfaceInteraction)) } + + return ShareGridTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: false) +} + +private class CancelButtonNode: ASDisplayNode { + let buttonNode: HighlightTrackingButtonNode + private let arrowNode: ASImageNode + private let labelNode: ImmediateTextNode + + var theme: PresentationTheme { + didSet { + self.updateThemeAndStrings() + } + } + private let strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + self.buttonNode = HighlightTrackingButtonNode() + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.isUserInteractionEnabled = false + + self.labelNode = ImmediateTextNode() + self.labelNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.buttonNode) + self.buttonNode.addSubnode(self.arrowNode) + self.buttonNode.addSubnode(self.labelNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.arrowNode.alpha = 0.4 + strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") + strongSelf.labelNode.alpha = 0.4 + } else { + strongSelf.arrowNode.alpha = 1.0 + strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.labelNode.alpha = 1.0 + strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + + self.updateThemeAndStrings() + } + + func updateThemeAndStrings() { + self.labelNode.attributedText = NSAttributedString(string: self.strings.Common_Back, font: Font.regular(17.0), textColor: self.theme.rootController.navigationBar.accentTextColor) + + let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) + + self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) + self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: self.theme.rootController.navigationBar.accentTextColor) + if let image = self.arrowNode.image { + self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) + } + self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize) + self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(11.0, 0.0, 0.0) + } + + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height)) + self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size) + self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size) + + return CGSize(width: 70.0, height: 56.0) + } +} + +final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { + func setEnsurePeerVisibleOnLayout(_ peerId: TelegramCore.EnginePeer.Id?) { + + } + + func updateSelectedPeers(animated: Bool) { + + } + + private let sharedContext: SharedAccountContext + private let context: AccountContext + private let theme: PresentationTheme + private let strings: PresentationStrings + private let controllerInteraction: ShareControllerInteraction + + private let disposable = MetaDisposable() + private var entries: [ShareTopicEntry] = [] + private var enqueuedTransitions: [(ShareGridTransaction, Bool)] = [] + + private let contentGridNode: GridNode + private let contentTitleNode: ASTextNode + private let contentSubtitleNode: ASTextNode + private let backNode: CancelButtonNode + + private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + + private var validLayout: (CGSize, CGFloat)? + private var overrideGridOffsetTransition: ContainedViewLayoutTransition? + + let peersValue = Promise<[EngineChatList.Item]>() + + var backPressed: () -> Void = {} + + init(sharedContext: SharedAccountContext, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, topics: [EngineChatList.Item], controllerInteraction: ShareControllerInteraction) { + self.sharedContext = sharedContext + self.context = context + self.theme = theme + self.strings = strings + self.controllerInteraction = controllerInteraction + + self.peersValue.set(.single(topics)) + + let items: Signal<[ShareTopicEntry], NoError> = self.peersValue.get() + |> map { topics -> [ShareTopicEntry] in + var entries: [ShareTopicEntry] = [] + var index: Int32 = 0 + + for topic in topics { + if case let .forum(_, _, threadId, _, _) = topic.index, let threadData = topic.threadData { + entries.append(ShareTopicEntry(index: index, peer: EngineRenderedPeer(peer: peer), id: threadId, threadData: threadData, theme: theme, strings: strings)) + index += 1 + } + } + return entries + } + + self.contentGridNode = GridNode() + + self.contentTitleNode = ASTextNode() + self.contentTitleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.medium(20.0), textColor: self.theme.actionSheet.primaryTextColor) + + self.contentSubtitleNode = ASTextNode() + self.contentSubtitleNode.maximumNumberOfLines = 1 + self.contentSubtitleNode.isUserInteractionEnabled = false + self.contentSubtitleNode.displaysAsynchronously = false + self.contentSubtitleNode.truncationMode = .byTruncatingTail + self.contentSubtitleNode.attributedText = NSAttributedString(string: strings.ShareMenu_SelectTopic, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor) + + self.backNode = CancelButtonNode(theme: theme, strings: strings) + + super.init() + + self.addSubnode(self.contentGridNode) + + self.addSubnode(self.contentTitleNode) + self.addSubnode(self.contentSubtitleNode) + self.addSubnode(self.backNode) + + let previousItems = Atomic<[ShareTopicEntry]?>(value: []) + self.disposable.set((items + |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + let previousEntries = previousItems.swap(entries) + strongSelf.entries = entries + + let firstTime = previousEntries == nil + let transition = preparedGridEntryTransition(context: context, from: previousEntries ?? [], to: entries, interfaceInteraction: controllerInteraction) + strongSelf.enqueueTransition(transition, firstTime: firstTime) + } + })) + + self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in + self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) + } + + self.backNode.buttonNode.addTarget(self, action: #selector(self.backButtonPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.disposable.dispose() + } + + @objc private func backButtonPressed() { + self.backPressed() + } + + private func enqueueTransition(_ transition: ShareGridTransaction, firstTime: Bool) { + self.enqueuedTransitions.append((transition, firstTime)) + + if self.validLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let (transition, _) = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var itemTransition: ContainedViewLayoutTransition = .immediate + if transition.animated { + itemTransition = .animated(duration: 0.3, curve: .spring) + } + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + } + } + + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { + self.contentOffsetUpdated = f + } + + private func calculateMetrics(size: CGSize) -> (topInset: CGFloat, itemWidth: CGFloat) { + let itemCount = self.entries.count + + let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) + let minimalItemWidth: CGFloat = size.width > 301.0 ? 70.0 : 60.0 + let effectiveWidth = size.width - itemInsets.left - itemInsets.right + + let itemsPerRow = Int(effectiveWidth / minimalItemWidth) + + let itemWidth = floor(effectiveWidth / CGFloat(itemsPerRow)) + var rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) + rowCount = max(rowCount, 4) + + let minimallyRevealedRowCount: CGFloat = 3.7 + let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) + + let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0) + return (gridTopInset, itemWidth) + } + + func activate() { + } + + func deactivate() { + } + + func animateIn(sourceFrame: CGRect) { + self.backNode.alpha = 1.0 + self.backNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.backNode.layer.animatePosition(from: CGPoint(x: 20.0, y: 0.0), to: .zero, duration: 0.2, additive: true) + + self.contentTitleNode.alpha = 1.0 + self.contentTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.contentTitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) + self.contentTitleNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.2) + + self.contentSubtitleNode.alpha = 1.0 + self.contentSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.contentSubtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) + self.contentSubtitleNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.2) + + self.contentGridNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + self.contentGridNode.forEachItemNode { itemNode in + itemNode.layer.animatePosition(from: sourceFrame.center, to: itemNode.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + itemNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func animateOut(targetFrame: CGRect, completion: @escaping () -> Void = {}) { + self.backNode.alpha = 0.0 + self.backNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.backNode.layer.animatePosition(from: .zero, to: CGPoint(x: 20.0, y: 0.0), duration: 0.2, additive: true) + + self.contentTitleNode.alpha = 0.0 + self.contentTitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.contentTitleNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 10.0), duration: 0.2, additive: true) + self.contentTitleNode.layer.animateScale(from: 1.0, to: 0.85, duration: 0.2) + + self.contentSubtitleNode.alpha = 0.0 + self.contentSubtitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.contentSubtitleNode.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 10.0), duration: 0.2, additive: true) + self.contentSubtitleNode.layer.animateScale(from: 1.0, to: 0.85, duration: 0.2) + + self.contentGridNode.alpha = 0.0 + self.contentGridNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in + completion() + }) + + self.contentGridNode.forEachItemNode { itemNode in + itemNode.layer.animatePosition(from: itemNode.position, to: targetFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + itemNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } + } + + func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + let firstLayout = self.validLayout == nil + self.validLayout = (size, bottomInset) + + let gridLayoutTransition: ContainedViewLayoutTransition + if firstLayout { + gridLayoutTransition = .immediate + self.overrideGridOffsetTransition = transition + } else { + gridLayoutTransition = transition + self.overrideGridOffsetTransition = nil + } + + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size) + + let scrollToItem: GridNodeScrollToItem? = nil + + let delta = bottomInset + var gridSize = CGSize(width: size.width - 12.0, height: size.height) + gridSize.height -= delta + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: gridTopInset, left: 0.0, bottom: 0.0, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: gridLayoutTransition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + gridLayoutTransition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((size.width - gridSize.width) / 2.0), y: 0.0), size: gridSize)) + + if firstLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let nodes: [ASDisplayNode] = [self.backNode] + for node in nodes { + let nodeFrame = node.frame + if node.isHidden { + continue + } + if let result = node.hitTest(point.offsetBy(dx: -nodeFrame.minX, dy: -nodeFrame.minY), with: event) { + return result + } + } + + return super.hitTest(point, with: event) + } + + private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) { + guard let (size, _) = self.validLayout else { + return + } + + let actualTransition = self.overrideGridOffsetTransition ?? transition + self.overrideGridOffsetTransition = nil + + let titleAreaHeight: CGFloat = 64.0 + + let rawTitleOffset = -titleAreaHeight - presentationLayout.contentOffset.y + let titleOffset = max(-titleAreaHeight, rawTitleOffset) + + let titleSize = self.contentTitleNode.measure(size) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: titleOffset + 15.0), size: titleSize) + transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) + + let subtitleSize = self.contentSubtitleNode.measure(CGSize(width: size.width - 44.0 * 2.0 - 8.0 * 2.0, height: titleAreaHeight)) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: titleOffset + 40.0), size: subtitleSize) + var originalSubtitleFrame = self.contentSubtitleNode.frame + originalSubtitleFrame.origin.x = subtitleFrame.origin.x + originalSubtitleFrame.size = subtitleFrame.size + self.contentSubtitleNode.frame = originalSubtitleFrame + transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame) + + let backFrame = CGRect(origin: CGPoint(x: 30.0, y: titleOffset + 6.0), size: CGSize(width: 90.0, height: 56.0)) + transition.updateFrame(node: self.backNode, frame: backFrame) + + self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition) + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index 7e3d8915e3..e563b4acc8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -712,6 +712,6 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) } - func updateSelectedPeers() { + func updateSelectedPeers(animated: Bool) { } } diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift index d4cf07ff4e..74d7524433 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift @@ -7,18 +7,40 @@ import TelegramPresentationData import LottieAnimationComponent public final class AudioTranscriptionButtonComponent: Component { + public enum Theme: Equatable { + public static func == (lhs: AudioTranscriptionButtonComponent.Theme, rhs: AudioTranscriptionButtonComponent.Theme) -> Bool { + switch lhs { + case let .bubble(lhsTheme): + if case let .bubble(rhsTheme) = lhs { + return lhsTheme === rhsTheme + } else { + return false + } + case let .freeform(lhsFreeform): + if case let .freeform(rhsFreeform) = rhs, lhsFreeform == rhsFreeform { + return true + } else { + return false + } + } + } + + case bubble(PresentationThemePartedColors) + case freeform((UIColor, Bool)) + } + public enum TranscriptionState { case inProgress case expanded case collapsed } - public let theme: PresentationThemePartedColors + public let theme: AudioTranscriptionButtonComponent.Theme public let transcriptionState: TranscriptionState public let pressed: () -> Void public init( - theme: PresentationThemePartedColors, + theme: AudioTranscriptionButtonComponent.Theme, transcriptionState: TranscriptionState, pressed: @escaping () -> Void ) { @@ -28,7 +50,7 @@ public final class AudioTranscriptionButtonComponent: Component { } public static func ==(lhs: AudioTranscriptionButtonComponent, rhs: AudioTranscriptionButtonComponent) -> Bool { - if lhs.theme !== rhs.theme { + if lhs.theme != rhs.theme { return false } if lhs.transcriptionState != rhs.transcriptionState { @@ -40,12 +62,14 @@ public final class AudioTranscriptionButtonComponent: Component { public final class View: UIButton { private var component: AudioTranscriptionButtonComponent? + private let blurredBackgroundNode: NavigationBackgroundNode private let backgroundLayer: SimpleLayer private let animationView: ComponentHostView private var progressAnimationView: ComponentHostView? override init(frame: CGRect) { + self.blurredBackgroundNode = NavigationBackgroundNode(color: .clear) self.backgroundLayer = SimpleLayer() self.animationView = ComponentHostView() @@ -73,7 +97,22 @@ public final class AudioTranscriptionButtonComponent: Component { func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { let size = CGSize(width: 30.0, height: 30.0) - let foregroundColor = component.theme.bubble.withWallpaper.reactionActiveBackground + let foregroundColor: UIColor + let backgroundColor: UIColor + switch component.theme { + case let .bubble(theme): + foregroundColor = theme.bubble.withWallpaper.reactionActiveBackground + backgroundColor = theme.bubble.withWallpaper.reactionInactiveBackground + case let .freeform(colorAndBlur): + foregroundColor = UIColor.white + backgroundColor = .clear + if self.blurredBackgroundNode.view.superview == nil { + self.insertSubview(self.blurredBackgroundNode.view, at: 0) + } + self.blurredBackgroundNode.updateColor(color: colorAndBlur.0, enableBlur: colorAndBlur.1, transition: .immediate) + self.blurredBackgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate) + self.blurredBackgroundNode.frame = CGRect(origin: .zero, size: size) + } if self.component?.transcriptionState != component.transcriptionState { switch component.transcriptionState { @@ -130,7 +169,7 @@ public final class AudioTranscriptionButtonComponent: Component { self.animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.width - animationSize.height) / 2.0)), size: animationSize) } - self.backgroundLayer.backgroundColor = component.theme.bubble.withWallpaper.reactionInactiveBackground.cgColor + self.backgroundLayer.backgroundColor = backgroundColor.cgColor self.component = component diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 9ca85762fd..684f9ad4ea 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -48,7 +48,7 @@ public final class EmojiStatusComponent: Component { case verified(fillColor: UIColor, foregroundColor: UIColor, sizeType: SizeType) case text(color: UIColor, string: String) case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor, themeColor: UIColor?, loopMode: LoopMode) - case topic(title: String, colorIndex: Int, size: CGSize) + case topic(title: String, color: Int32, size: CGSize) } public let context: AccountContext @@ -223,7 +223,7 @@ public final class EmojiStatusComponent: Component { } else { iconImage = nil } - case let .topic(title, colorIndex, size): + case let .topic(title, color, size): func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? { return generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) @@ -283,18 +283,21 @@ public final class EmojiStatusComponent: Component { }) } - let topicColors: [([UInt32], [UInt32])] = [ - ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), - ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), - ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), - ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), - ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), - ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), - ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) - ] - let clippedIndex = colorIndex % topicColors.count + func generateTopicColors(_ color: Int32) -> ([UInt32], [UInt32]) { + return ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) + } - if let image = generateTopicIcon(backgroundColors: topicColors[clippedIndex].0.map(UIColor.init(rgb:)), strokeColors: topicColors[clippedIndex].1.map(UIColor.init(rgb:))) { + let topicColors: [Int32: ([UInt32], [UInt32])] = [ + 0x6FB9F0: ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), + 0xFFD67E: ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), + 0xCB86DB: ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), + 0x8EEE98: ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), + 0xFF93B2: ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), + 0xFB6F5F: ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) + ] + let colors = topicColors[color] ?? generateTopicColors(color) + + if let image = generateTopicIcon(backgroundColors: colors.0.map(UIColor.init(rgb:)), strokeColors: colors.1.map(UIColor.init(rgb:))) { iconImage = image } else { iconImage = nil diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 13253c81d5..5e89f64eb0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2145,7 +2145,7 @@ public final class EmojiPagerContentComponent: Component { public enum Icon: Equatable, Hashable { case premiumStar - case topic(String) + case topic(String, Int32) } case animation(EntityKeyboardAnimationData) @@ -2943,8 +2943,9 @@ public final class EmojiPagerContentComponent: Component { let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } - case let .topic(title): - if let image = generateTopicIcon(backgroundColors: [UIColor(rgb: 0x6FB9F0), UIColor(rgb: 0x0261E4)], strokeColors: [UIColor(rgb: 0x026CB5), UIColor(rgb: 0x064BB7)], title: title) { + case let .topic(title, color): + let colors = self.getTopicColors(color) + if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) { let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } @@ -2992,16 +2993,30 @@ public final class EmojiPagerContentComponent: Component { return nullAction } + private func getTopicColors(_ color: Int32) -> ([UInt32], [UInt32]) { + let topicColors: [Int32: ([UInt32], [UInt32])] = [ + 0x6FB9F0: ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), + 0xFFD67E: ([0xFFD67E, 0xFC8601], [0xDA9400, 0xFA5F00]), + 0xCB86DB: ([0xCB86DB, 0x9338AF], [0x812E98, 0x6F2B87]), + 0x8EEE98: ([0x8EEE98, 0x02B504], [0x02A01B, 0x009716]), + 0xFF93B2: ([0xFF93B2, 0xE23264], [0xFC447A, 0xC80C46]), + 0xFB6F5F: ([0xFB6F5F, 0xD72615], [0xDC1908, 0xB61506]) + ] + + return topicColors[color] ?? ([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]) + } + func update(content: ItemContent) { if self.content != content { - if case let .icon(icon) = content, case let .topic(title) = icon { + if case let .icon(icon) = content, case let .topic(title, color) = icon { let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) - if let image = generateTopicIcon(backgroundColors: [UIColor(rgb: 0x6FB9F0), UIColor(rgb: 0x0261E4)], strokeColors: [UIColor(rgb: 0x026CB5), UIColor(rgb: 0x064BB7)], title: title) { - let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) + let colors = self.getTopicColors(color) + if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) { + let imageSize = image.size image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } @@ -6195,7 +6210,8 @@ public final class EmojiPagerContentComponent: Component { chatPeerId: EnginePeer.Id?, selectedItems: Set = Set(), topStatusTitle: String? = nil, - topicTitle: String? = nil + topicTitle: String? = nil, + topicColor: Int32? = nil ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -6279,7 +6295,7 @@ public final class EmojiPagerContentComponent: Component { if isTopicIconSelection { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, - content: .icon(.topic(String((topicTitle ?? "").prefix(1)))), + content: .icon(.topic(String((topicTitle ?? "").prefix(1)), topicColor ?? 0)), itemFile: nil, subgroupId: nil, icon: .none, diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 8434f4f60d..a3c7b97e87 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -23,6 +23,7 @@ private final class TitleFieldComponent: Component { let accentColor: UIColor let placeholderColor: UIColor let fileId: Int64 + let iconColor: Int32 let text: String let textUpdated: (String) -> Void @@ -32,6 +33,7 @@ private final class TitleFieldComponent: Component { accentColor: UIColor, placeholderColor: UIColor, fileId: Int64, + iconColor: Int32, text: String, textUpdated: @escaping (String) -> Void ) { @@ -40,6 +42,7 @@ private final class TitleFieldComponent: Component { self.accentColor = accentColor self.placeholderColor = placeholderColor self.fileId = fileId + self.iconColor = iconColor self.text = text self.textUpdated = textUpdated } @@ -60,6 +63,9 @@ private final class TitleFieldComponent: Component { if lhs.fileId != rhs.fileId { return false } + if lhs.iconColor != rhs.iconColor { + return false + } if lhs.text != rhs.text { return false } @@ -106,7 +112,7 @@ private final class TitleFieldComponent: Component { let iconContent: EmojiStatusComponent.Content if component.fileId == 0 { - iconContent = .topic(title: String(component.text.prefix(1)), colorIndex: 0, size: CGSize(width: 32.0, height: 32.0)) + iconContent = .topic(title: String(component.text.prefix(1)), color: component.iconColor, size: CGSize(width: 32.0, height: 32.0)) } else { iconContent = .animation(content: .customEmoji(fileId: component.fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: component.placeholderColor, themeColor: component.accentColor, loopMode: .count(2)) } @@ -266,7 +272,7 @@ private final class TopicIconSelectionComponent: Component { deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, displayBottomPanel: false, - isExpanded: false + isExpanded: true )), environment: {}, containerSize: availableSize @@ -347,6 +353,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { var title: String var fileId: Int64 + var iconColor: Int32 init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void) { self.context = context @@ -354,12 +361,16 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { self.iconUpdated = iconUpdated switch mode { - case .create: - self.title = "" - self.fileId = 0 - case let .edit(info): - self.title = info.title - self.fileId = info.icon ?? 0 + case .create: + self.title = "" + self.fileId = 0 + + let colors: [Int32] = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98,0xFF93B2, 0xFB6F5F] + self.iconColor = colors.randomElement() ?? 0x0 + case let .edit(info): + self.title = info.title + self.fileId = info.icon ?? 0 + self.iconColor = info.iconColor } super.init() @@ -378,7 +389,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId, selectedItems: Set(), - topicTitle: self.title + topicTitle: self.title, + topicColor: self.iconColor ) |> deliverOnMainQueue).start(next: { [weak self] content in self?.emojiContent = content @@ -410,7 +422,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId, selectedItems: Set([MediaId(namespace: Namespaces.Media.CloudFile, id: self.fileId)]), - topicTitle: self.title + topicTitle: self.title, + topicColor: self.iconColor ) |> deliverOnMainQueue).start(next: { [weak self] content in self?.emojiContent = content @@ -442,7 +455,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { areCustomEmojiEnabled: true, chatPeerId: self.context.account.peerId, selectedItems: Set([MediaId(namespace: Namespaces.Media.CloudFile, id: self.fileId)]), - topicTitle: self.title + topicTitle: self.title, + topicColor: self.iconColor ) |> deliverOnMainQueue).start(next: { [weak self] content in self?.emojiContent = content @@ -532,6 +546,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { accentColor: environment.theme.list.itemAccentColor, placeholderColor: environment.theme.list.disclosureArrowColor, fileId: state.fileId, + iconColor: state.iconColor, text: state.title, textUpdated: { [weak state] text in state?.updateTitle(text) @@ -568,6 +583,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { ) contentHeight += iconHeader.size.height + headerSpacing + let bottomInset = max(environment.safeInsets.bottom, 12.0) + let iconBackground = iconBackground.update( component: RoundedRectangle( color: environment.theme.list.itemBlocksBackgroundColor, @@ -575,7 +592,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { ), availableSize: CGSize( width: context.availableSize.width - sideInset * 2.0, - height: context.availableSize.height - topInset - titleHeader.size.height - titleBackground.size.height + height: context.availableSize.height - contentHeight - bottomInset ), transition: context.transition ) @@ -584,7 +601,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { ) if let emojiContent = state.emojiContent { - let availableHeight = context.availableSize.height - contentHeight - environment.inputHeight + let availableHeight = context.availableSize.height - contentHeight - max(bottomInset, environment.inputHeight) let iconSelector = iconSelector.update( component: TopicIconSelectionComponent( @@ -592,7 +609,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { strings: environment.strings, deviceMetrics: environment.deviceMetrics, emojiContent: emojiContent, - backgroundColor: .clear, + backgroundColor: environment.theme.list.itemBlocksBackgroundColor, separatorColor: environment.theme.list.blocksBackgroundColor ), environment: {}, @@ -601,6 +618,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { ) context.add(iconSelector .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + iconSelector.size.height / 2.0)) + .cornerRadius(10.0) + .clipsToBounds(true) ) let accountContext = context.component.context @@ -736,8 +755,6 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { } @objc private func createPressed() { -// self.dismiss() - self.completion(self.state.0, self.state.1) } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index acbc310568..bc3d78994d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -733,8 +733,28 @@ private func extractAccountManagerState(records: AccountRecordsView(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index cdbfb325d6..5761d0c409 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4828,7 +4828,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let fileId = threadInfo.icon { avatarContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: strongSelf.presentationData.theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .count(2)) } else { - avatarContent = .topic(title: String(threadInfo.title.prefix(1)), colorIndex: Int(threadInfo.iconColor), size: CGSize(width: 32.0, height: 32.0)) + avatarContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: 32.0, height: 32.0)) } (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent) } else { diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index f1444d50e0..6853a60f64 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -838,11 +838,11 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, if let fileId = self.fileId { iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 54.0, height: 54.0), placeholderColor: .clear, themeColor: serviceColor.primaryText, loopMode: .count(2)) } else { - var colorIndex: Int = 0 - if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation { - colorIndex = Int(clamping: abs(replyThreadMessage.effectiveTopId.id)) - } - iconContent = .topic(title: String(title.prefix(1)), colorIndex: colorIndex, size: CGSize(width: 64.0, height: 64.0)) + let colorIndex: Int32 = 0 +// if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation { +// colorIndex = Int(clamping: abs(replyThreadMessage.effectiveTopId.id)) +// } + iconContent = .topic(title: String(title.prefix(1)), color: colorIndex, size: CGSize(width: 64.0, height: 64.0)) } let insets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) diff --git a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift b/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift index e112eb6fa7..582cfc2b4b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift @@ -173,14 +173,14 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode { var lineWidth = self.progressLayer.lineWidth self.progressLayer.lineWidth = 1.0 - self.progressLayer.animate(from: lineWidth as NSNumber, to: 1.0 as NSNumber, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.25, completion: { [weak self] _ in - self?.progressLayer.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15, removeOnCompletion: false) + self.progressLayer.animate(from: lineWidth as NSNumber, to: 1.0 as NSNumber, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, completion: { [weak self] _ in + self?.progressLayer.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "lineWidth", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.1, removeOnCompletion: false) }) var path = self.progressLayer.path var targetPath = UIBezierPath(arcCenter: CGPoint(x: self.progressLayer.frame.width / 2.0, y: self.progressLayer.frame.height / 2.0), radius: 35.0, startAngle: CGFloat(-0.5 * .pi), endAngle: CGFloat(-0.5 * .pi + 2.0 * .pi), clockwise: true).cgPath self.progressLayer.path = targetPath - self.progressLayer.animate(from: path, to: targetPath, keyPath: "path", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + self.progressLayer.animate(from: path, to: targetPath, keyPath: "path", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2) self.fillLayer.isHidden = false self.fillLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 8dbd8c62c4..9cc85d6a2c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -425,7 +425,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if let iconFileId = threadInfo.icon { content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever) } else { - content = .topic(title: String(threadInfo.title.prefix(1)), colorIndex: Int(threadInfo.iconColor), size: CGSize(width: avatarSize, height: avatarSize)) + content = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: avatarSize, height: avatarSize)) } let _ = iconView.update( transition: .immediate,