Various improvements

This commit is contained in:
Ilya Laktyushin 2022-10-19 00:23:15 +03:00
parent 85ac817816
commit 3d5218b3f6
31 changed files with 1225 additions and 120 deletions

View File

@ -8142,3 +8142,5 @@ Sorry for the inconvenience.";
"Channel.AdminLog.MessageChangedGroupUsernames" = "%@ changed group links:"; "Channel.AdminLog.MessageChangedGroupUsernames" = "%@ changed group links:";
"Channel.AdminLog.MessageChangedChannelUsernames" = "%@ changed channel links:"; "Channel.AdminLog.MessageChangedChannelUsernames" = "%@ changed channel links:";
"Channel.AdminLog.MessagePreviousLinks" = "Previous links"; "Channel.AdminLog.MessagePreviousLinks" = "Previous links";
"ShareMenu.SelectTopic" = "Select topic";

View File

@ -2018,7 +2018,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let fileId = threadInfo.info.icon, fileId != 0 { 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) 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 { } 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( let avatarIconComponent = EmojiStatusComponent(

View File

@ -1053,7 +1053,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
if let fileId = icon, fileId != 0 { 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) 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 { } 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( let avatarIconComponent = EmojiStatusComponent(

View File

@ -21,7 +21,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
case iPhone13Pro case iPhone13Pro
case iPhone13ProMax case iPhone13ProMax
case iPhone14Pro case iPhone14Pro
case iPhone14ProZoomed
case iPhone14ProMax case iPhone14ProMax
case iPhone14ProMaxZoomed
case iPad case iPad
case iPadMini case iPadMini
case iPad102Inch case iPad102Inch
@ -49,7 +51,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
.iPhone13Pro, .iPhone13Pro,
.iPhone13ProMax, .iPhone13ProMax,
.iPhone14Pro, .iPhone14Pro,
.iPhone14ProZoomed,
.iPhone14ProMax, .iPhone14ProMax,
.iPhone14ProMaxZoomed,
.iPad, .iPad,
.iPadMini, .iPadMini,
.iPad102Inch, .iPad102Inch,
@ -83,7 +87,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
let width = device.screenSize.width let width = device.screenSize.width
let height = device.screenSize.height 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 ((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 self = .iPhoneXr
} else { } else {
self = device self = device
@ -135,8 +141,12 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return CGSize(width: 428.0, height: 926.0) return CGSize(width: 428.0, height: 926.0)
case .iPhone14Pro: case .iPhone14Pro:
return CGSize(width: 393.0, height: 852.0) return CGSize(width: 393.0, height: 852.0)
case .iPhone14ProZoomed:
return CGSize(width: 320.0, height: 693.0)
case .iPhone14ProMax: case .iPhone14ProMax:
return CGSize(width: 430.0, height: 932.0) return CGSize(width: 430.0, height: 932.0)
case .iPhone14ProMaxZoomed:
return CGSize(width: 375.0, height: 812.0)
case .iPad: case .iPad:
return CGSize(width: 768.0, height: 1024.0) return CGSize(width: 768.0, height: 1024.0)
case .iPadMini: case .iPadMini:
@ -164,9 +174,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 41.0 + UIScreenPixel return 41.0 + UIScreenPixel
case .iPhone12Mini: case .iPhone12Mini:
return 44.0 return 44.0
case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro: case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed:
return 47.0 + UIScreenPixel return 47.0 + UIScreenPixel
case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone14ProMaxZoomed:
return 53.0 + UIScreenPixel return 53.0 + UIScreenPixel
case let .unknown(_, _, onScreenNavigationHeight): case let .unknown(_, _, onScreenNavigationHeight):
if let _ = onScreenNavigationHeight { if let _ = onScreenNavigationHeight {
@ -181,7 +191,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func safeInsets(inLandscape: Bool) -> UIEdgeInsets { func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
switch self { 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) 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: default:
return UIEdgeInsets.zero return UIEdgeInsets.zero
@ -192,6 +202,10 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self { 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, .iPhone14ProMax:
return inLandscape ? 21.0 : 34.0 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: case .iPadPro3rdGen, .iPadPro11Inch:
return 21.0 return 21.0
case .iPad, .iPadPro, .iPadPro10Inch, .iPadMini, .iPadMini6thGen: case .iPad, .iPadPro, .iPadPro10Inch, .iPadMini, .iPadMini6thGen:
@ -224,6 +238,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self { switch self {
case .iPhone14Pro, .iPhone14ProMax: case .iPhone14Pro, .iPhone14ProMax:
return 54.0 return 54.0
case .iPhone14ProMaxZoomed:
return 47.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
return 44.0 return 44.0
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
@ -242,7 +258,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 162.0 return 162.0
case .iPhone6, .iPhone6Plus: case .iPhone6, .iPhone6Plus:
return 163.0 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 return 172.0
case .iPad, .iPad102Inch, .iPadPro10Inch: case .iPad, .iPad102Inch, .iPadPro10Inch:
return 348.0 return 348.0
@ -261,7 +277,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 216.0 return 216.0
case .iPhone6Plus: case .iPhone6Plus:
return 226.0 return 226.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro: case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
return 292.0 return 292.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax: case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
return 302.0 return 302.0
@ -282,7 +298,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func predictiveInputHeight(inLandscape: Bool) -> CGFloat { func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape { if inLandscape {
switch self { 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 return 37.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 50.0 return 50.0
@ -293,7 +309,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self { switch self {
case .iPhone4, .iPhone5: case .iPhone4, .iPhone5:
return 37.0 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 return 44.0
case .iPhone6Plus: case .iPhone6Plus:
return 45.0 return 45.0
@ -316,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public var hasDynamicIsland: Bool { public var hasDynamicIsland: Bool {
switch self { switch self {
case .iPhone14Pro, .iPhone14ProMax: case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
return true return true
default: default:
return false return false

View File

@ -85,16 +85,24 @@ private final class WindowRootViewController: UIViewController, UIViewController
didSet { didSet {
if oldValue != self.orientations { if oldValue != self.orientations {
if self.orientations == .portrait { 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 let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation") UIDevice.current.setValue(value, forKey: "orientation")
} }
} else {
if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
self.setNeedsUpdateOfSupportedInterfaceOrientations()
} else { } else {
UIViewController.attemptRotationToDeviceOrientation() UIViewController.attemptRotationToDeviceOrientation()
} }
} }
} }
} }
}
var gestureEdges: UIRectEdge = [] { var gestureEdges: UIRectEdge = [] {
didSet { didSet {

View File

@ -160,7 +160,7 @@ open class NavigationController: UINavigationController, ContainableController,
public private(set) var validLayout: ContainerViewLayout? public private(set) var validLayout: ContainerViewLayout?
private var validStatusBarStyle: NavigationStatusBarStyle? private var validStatusBarStyle: NavigationStatusBarStyle?
private var validStatusBarHidden: Bool = false private var validStatusBarHidden: Bool?
private var ignoreInputHeight: Bool = false private var ignoreInputHeight: Bool = false
private var currentStatusBarExternalHidden: Bool = false private var currentStatusBarExternalHidden: Bool = false

View File

@ -270,7 +270,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
self.contentOffsetUpdated?(-size.height + nodeHeight, transition) 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) self.contentOffsetUpdated?(-size.height + nodeHeight, transition)
} }
public func updateSelectedPeers() { public func updateSelectedPeers(animated: Bool) {
} }
} }

View File

@ -110,6 +110,6 @@ final class LanguageLinkPreviewContentNode: ASDisplayNode, ShareContentContainer
self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition)
} }
func updateSelectedPeers() { func updateSelectedPeers(animated: Bool) {
} }
} }

View File

@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 226.0 self.topOffset = 226.0
self.biometricsOffset = 30.0 self.biometricsOffset = 30.0
self.deleteOffset = 20.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.buttonSize = 75.0
self.horizontalSecond = 103.0 self.horizontalSecond = 103.0
self.horizontalThird = 206.0 self.horizontalThird = 206.0
@ -151,7 +151,7 @@ public struct PasscodeLayout {
self.titleOffset = 112.0 self.titleOffset = 112.0
self.subtitleOffset = -6.0 self.subtitleOffset = -6.0
self.inputFieldOffset = 156.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.titleOffset = 162.0
self.subtitleOffset = 0.0 self.subtitleOffset = 0.0
self.inputFieldOffset = 206.0 self.inputFieldOffset = 206.0

View File

@ -22,6 +22,8 @@ swift_library(
"//submodules/LocalizedPeerData:LocalizedPeerData", "//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/CheckNode:CheckNode", "//submodules/CheckNode:CheckNode",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -12,6 +12,8 @@ import ContextUI
import LocalizedPeerData import LocalizedPeerData
import AccountContext import AccountContext
import CheckNode import CheckNode
import ComponentFlow
import EmojiStatusComponent
private let avatarFont = avatarPlaceholderFont(size: 24.0) private let avatarFont = avatarPlaceholderFont(size: 24.0)
private let textFont = Font.regular(11.0) private let textFont = Font.regular(11.0)
@ -73,6 +75,8 @@ public final class SelectablePeerNode: ASDisplayNode {
private var checkNode: CheckNode? private var checkNode: CheckNode?
private let textNode: ASTextNode private let textNode: ASTextNode
private let iconView: ComponentView<Empty>
public var toggleSelection: (() -> Void)? public var toggleSelection: (() -> Void)?
public var contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? { public var contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? {
didSet { didSet {
@ -119,6 +123,8 @@ public final class SelectablePeerNode: ASDisplayNode {
self.onlineNode = PeerOnlineMarkerNode() self.onlineNode = PeerOnlineMarkerNode()
self.iconView = ComponentView<Empty>()
super.init() super.init()
self.addSubnode(self.contextContainer) 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 let isFirstTime = self.peer == nil
self.peer = peer self.peer = peer
guard let mainPeer = peer.chatMainPeer else { guard let mainPeer = peer.chatMainPeer else {
@ -166,7 +172,7 @@ public final class SelectablePeerNode: ASDisplayNode {
} }
} }
self.textNode.maximumNumberOfLines = numberOfLines 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) 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() 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.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel), color: nil, transition: .immediate)
self.onlineNode.frame = CGRect(origin: CGPoint(), size: onlineSize) 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() self.setNeedsLayout()
} }
@ -267,7 +307,18 @@ public final class SelectablePeerNode: ASDisplayNode {
self.contextContainer.frame = bounds 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.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 avatarFrame = self.avatarNode.frame
let avatarContainerFrame = self.avatarNodeContainer.frame let avatarContainerFrame = self.avatarNodeContainer.frame

View File

@ -35,6 +35,8 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -10,5 +10,5 @@ public protocol ShareContentContainerNode: AnyObject {
func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?)
func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?)
func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition)
func updateSelectedPeers() func updateSelectedPeers(animated: Bool)
} }

View File

@ -1011,12 +1011,14 @@ public final class ShareController: ViewController {
return (resultPeers, accountPeer) return (resultPeers, accountPeer)
} }
}) })
var animatedIn = false
self.peersDisposable.set((self.peers.get() self.peersDisposable.set((self.peers.get()
|> deliverOnMainQueue).start(next: { [weak self] next in |> deliverOnMainQueue).start(next: { [weak self] next in
if let strongSelf = self { 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) 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() strongSelf.readyDisposable.set((strongSelf.controllerNode.ready.get()
|> filter({ $0 }) |> filter({ $0 })
|> take(1) |> take(1)

View File

@ -73,6 +73,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private var controllerInteraction: ShareControllerInteraction? private var controllerInteraction: ShareControllerInteraction?
private var peersContentNode: SharePeersContainerNode? private var peersContentNode: SharePeersContainerNode?
private var topicsContentNode: ShareTopicsContainerNode?
private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequestId: Int = 0
private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)?
@ -244,15 +245,28 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer, search in self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer, search in
if let strongSelf = self { if let strongSelf = self {
var added = false var added = false
var openedTopic = false
if strongSelf.controllerInteraction!.selectedPeerIds.contains(peer.peerId) { if strongSelf.controllerInteraction!.selectedPeerIds.contains(peer.peerId) {
strongSelf.controllerInteraction!.selectedTopics[peer.peerId] = nil
strongSelf.peersContentNode?.update()
strongSelf.controllerInteraction!.selectedPeerIds.remove(peer.peerId) strongSelf.controllerInteraction!.selectedPeerIds.remove(peer.peerId)
strongSelf.controllerInteraction!.selectedPeers = strongSelf.controllerInteraction!.selectedPeers.filter({ $0.peerId != peer.peerId }) strongSelf.controllerInteraction!.selectedPeers = strongSelf.controllerInteraction!.selectedPeers.filter({ $0.peerId != peer.peerId })
} else {
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 { } else {
strongSelf.controllerInteraction!.selectedPeerIds.insert(peer.peerId) strongSelf.controllerInteraction!.selectedPeerIds.insert(peer.peerId)
strongSelf.controllerInteraction!.selectedPeers.append(peer) strongSelf.controllerInteraction!.selectedPeers.append(peer)
added = true
}
strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.peerId) strongSelf.contentNode?.setEnsurePeerVisibleOnLayout(peer.peerId)
added = true
} }
if search && added { if search && added {
@ -263,12 +277,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
strongSelf.peersContentNode?.updateFoundPeers() strongSelf.peersContentNode?.updateFoundPeers()
} }
if !openedTopic {
strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil) strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil)
strongSelf.updateButton() strongSelf.updateButton()
strongSelf.peersContentNode?.updateSelectedPeers() strongSelf.peersContentNode?.updateSelectedPeers(animated: true)
strongSelf.contentNode?.updateSelectedPeers() strongSelf.contentNode?.updateSelectedPeers(animated: true)
}
if let (layout, navigationBarHeight, _) = strongSelf.containerLayout { if let (layout, navigationBarHeight, _) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) 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 self.backgroundColor = nil
@ -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) { func updatePresentationData(_ presentationData: PresentationData) {
guard self.presentationData !== presentationData else { guard self.presentationData !== presentationData else {
return 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)) 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) 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) { private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {
@ -967,7 +1063,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
private func updateButton() { private func updateButton() {
if self.controllerInteraction!.selectedPeers.isEmpty { let count = self.controllerInteraction!.selectedPeerIds.count
if count == 0 {
if self.presetText != nil { 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.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal)
self.actionButtonNode.isEnabled = false self.actionButtonNode.isEnabled = false
@ -990,13 +1087,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
let text: String let text: String
if let segmentedValues = self.segmentedValues { if let segmentedValues = self.segmentedValues {
let value = segmentedValues[self.selectedSegmentedIndex] let value = segmentedValues[self.selectedSegmentedIndex]
text = value.formatSendTitle(self.controllerInteraction!.selectedPeers.count) text = value.formatSendTitle(count)
} else { } else {
text = self.presentationData.strings.ShareMenu_Send text = self.presentationData.strings.ShareMenu_Send
} }
self.actionButtonNode.isEnabled = true self.actionButtonNode.isEnabled = true
self.actionButtonNode.setTitle(text, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) 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) return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, customPosition: self.customPosition)
} }
} }
private func threadList(context: AccountContext, peerId: EnginePeer.Id) -> Signal<EngineChatList, NoError> {
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
}
}

View File

@ -16,10 +16,15 @@ final class ShareControllerInteraction {
var foundPeers: [EngineRenderedPeer] = [] var foundPeers: [EngineRenderedPeer] = []
var selectedPeerIds = Set<PeerId>() var selectedPeerIds = Set<PeerId>()
var selectedPeers: [EngineRenderedPeer] = [] 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.togglePeer = togglePeer
self.selectTopic = selectTopic
} }
} }
@ -92,17 +97,21 @@ final class ShareControllerPeerGridItem: GridItem {
let strings: PresentationStrings let strings: PresentationStrings
let peer: EngineRenderedPeer? let peer: EngineRenderedPeer?
let presence: EnginePeer.Presence? let presence: EnginePeer.Presence?
let topicId: Int64?
let threadData: MessageHistoryThreadData?
let controllerInteraction: ShareControllerInteraction let controllerInteraction: ShareControllerInteraction
let search: Bool let search: Bool
let section: GridSection? 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.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.peer = peer self.peer = peer
self.presence = presence self.presence = presence
self.topicId = topicId
self.threadData = threadData
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.search = search self.search = search
@ -116,7 +125,7 @@ final class ShareControllerPeerGridItem: GridItem {
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ShareControllerPeerGridItemNode() let node = ShareControllerPeerGridItemNode()
node.controllerInteraction = self.controllerInteraction 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 return node
} }
@ -126,12 +135,12 @@ final class ShareControllerPeerGridItem: GridItem {
return return
} }
node.controllerInteraction = self.controllerInteraction 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 { 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 let peerNode: SelectablePeerNode
private var presenceManager: PeerPresenceStatusManager? private var presenceManager: PeerPresenceStatusManager?
@ -140,6 +149,10 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
private var placeholderNode: ShimmerEffectNode? private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)? private var absoluteLocation: (CGRect, CGSize)?
var peerId: EnginePeer.Id? {
return self.currentState?.3?.peerId
}
override init() { override init() {
self.peerNode = SelectablePeerNode() self.peerNode = SelectablePeerNode()
@ -147,7 +160,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
self.peerNode.toggleSelection = { [weak self] in self.peerNode.toggleSelection = { [weak self] in
if let strongSelf = self { 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] { if let _ = peer.peers[peer.peerId] {
strongSelf.controllerInteraction?.togglePeer(peer, search) strongSelf.controllerInteraction?.togglePeer(peer, search)
} }
@ -159,7 +172,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
guard let strongSelf = self, let currentState = strongSelf.currentState else { guard let strongSelf = self, let currentState = strongSelf.currentState else {
return 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) { 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 { 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 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) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
@ -186,7 +199,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
self.peerNode.theme = itemTheme self.peerNode.theme = itemTheme
if let peer = peer { 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 { if let shimmerNode = self.placeholderNode {
self.placeholderNode = nil self.placeholderNode = nil
shimmerNode.removeFromSupernode() 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) 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() self.setNeedsLayout()
if let presence = presence { if let presence = presence {
self.presenceManager?.reset(presence: presence) self.presenceManager?.reset(presence: presence)
@ -229,7 +242,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
func updateSelection(animated: Bool) { func updateSelection(animated: Bool) {
var selected = false 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) selected = controllerInteraction.selectedPeerIds.contains(peer.peerId)
} }
@ -243,7 +256,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
self.peerNode.frame = bounds self.peerNode.frame = bounds
self.placeholderNode?.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] = [] var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 56.0 let titleLineWidth: CGFloat = 56.0

View File

@ -91,7 +91,7 @@ public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContain
self.contentOffsetUpdated?(-size.height + 64.0, transition) 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) self.contentOffsetUpdated?(-size.height + nodeHeight * 0.5, transition)
} }
public func updateSelectedPeers() { public func updateSelectedPeers(animated: Bool) {
} }
} }

View File

@ -17,10 +17,31 @@ import ContextUI
private let subtitleFont = Font.regular(12.0) 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 { private struct SharePeerEntry: Comparable, Identifiable {
let index: Int32 let index: Int32
let peer: EngineRenderedPeer let peer: EngineRenderedPeer
let presence: EnginePeer.Presence? let presence: EnginePeer.Presence?
let threadId: Int64?
let threadData: MessageHistoryThreadData?
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
@ -38,6 +59,12 @@ private struct SharePeerEntry: Comparable, Identifiable {
if lhs.presence != rhs.presence { if lhs.presence != rhs.presence {
return false return false
} }
if lhs.threadId != rhs.threadId {
return false
}
if lhs.threadData != rhs.threadData {
return false
}
return true return true
} }
@ -47,7 +74,7 @@ private struct SharePeerEntry: Comparable, Identifiable {
} }
func item(context: AccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem { 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?)]>() let peersValue = Promise<[(EngineRenderedPeer, EnginePeer.Presence?)]>()
private var _tick: Int = 0 {
didSet {
self.tick.set(self._tick)
}
}
private let tick = ValuePromise<Int>(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]?) { 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.sharedContext = sharedContext
self.context = context self.context = context
@ -129,19 +163,19 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.peersValue.set(.single(peers)) self.peersValue.set(.single(peers))
let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get()) let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get(), self.tick.get())
|> map { initialPeers, foundPeers -> [SharePeerEntry] in |> map { [weak controllerInteraction] initialPeers, foundPeers, _ -> [SharePeerEntry] in
var entries: [SharePeerEntry] = [] var entries: [SharePeerEntry] = []
var index: Int32 = 0 var index: Int32 = 0
var existingPeerIds: Set<PeerId> = Set() var existingPeerIds: Set<PeerId> = 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) existingPeerIds.insert(accountPeer.id)
index += 1 index += 1
for peer in foundPeers.reversed() { for peer in foundPeers.reversed() {
if !existingPeerIds.contains(peer.peerId) { 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) existingPeerIds.insert(peer.peerId)
index += 1 index += 1
} }
@ -149,7 +183,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
for (peer, presence) in initialPeers { for (peer, presence) in initialPeers {
if !existingPeerIds.contains(peer.peerId) { 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) existingPeerIds.insert(peer.peerId)
index += 1 index += 1
} }
@ -325,6 +360,154 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
func deactivate() { 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) { func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let firstLayout = self.validLayout == nil let firstLayout = self.validLayout == nil
self.validLayout = (size, bottomInset) 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 }) 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)) 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)) self.foundPeers.set(.single(self.controllerInteraction.foundPeers))
} }
func updateSelectedPeers() { func update() {
self._tick += 1
}
func updateSelectedPeers(animated: Bool = true) {
if self.segmentedValues != nil { if self.segmentedValues != nil {
self.contentTitleNode.isHidden = true self.contentTitleNode.isHidden = true
self.contentSubtitleNode.isHidden = true self.contentSubtitleNode.isHidden = true
@ -453,7 +640,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
} }
self.contentGridNode.forEachItemNode { itemNode in self.contentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ShareControllerPeerGridItemNode { if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
itemNode.updateSelection(animated: true) itemNode.updateSelection(animated: animated)
} }
} }
} }

View File

@ -94,7 +94,7 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
peers[associatedPeer.id] = associatedPeer peers[associatedPeer.id] = associatedPeer
} }
let peer = EngineRenderedPeer(RenderedPeer(peerId: peer.id, peers: SimpleDictionary(peers), associatedMedia: [:])) 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 { 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 animateIn() {
} }
func updateSelectedPeers() { func updateSelectedPeers(animated: Bool) {
self.contentGridNode.forEachItemNode { itemNode in self.contentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ShareControllerPeerGridItemNode { if let itemNode = itemNode as? ShareControllerPeerGridItemNode {
itemNode.updateSelection(animated: true) itemNode.updateSelection(animated: true)

View File

@ -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<Empty>
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<Empty>()
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()
}
}

View File

@ -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)
}
}

View File

@ -712,6 +712,6 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod
self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition) self.contentOffsetUpdated?(-size.height + nodeHeight - 64.0, transition)
} }
func updateSelectedPeers() { func updateSelectedPeers(animated: Bool) {
} }
} }

View File

@ -7,18 +7,40 @@ import TelegramPresentationData
import LottieAnimationComponent import LottieAnimationComponent
public final class AudioTranscriptionButtonComponent: Component { 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 { public enum TranscriptionState {
case inProgress case inProgress
case expanded case expanded
case collapsed case collapsed
} }
public let theme: PresentationThemePartedColors public let theme: AudioTranscriptionButtonComponent.Theme
public let transcriptionState: TranscriptionState public let transcriptionState: TranscriptionState
public let pressed: () -> Void public let pressed: () -> Void
public init( public init(
theme: PresentationThemePartedColors, theme: AudioTranscriptionButtonComponent.Theme,
transcriptionState: TranscriptionState, transcriptionState: TranscriptionState,
pressed: @escaping () -> Void pressed: @escaping () -> Void
) { ) {
@ -28,7 +50,7 @@ public final class AudioTranscriptionButtonComponent: Component {
} }
public static func ==(lhs: AudioTranscriptionButtonComponent, rhs: AudioTranscriptionButtonComponent) -> Bool { public static func ==(lhs: AudioTranscriptionButtonComponent, rhs: AudioTranscriptionButtonComponent) -> Bool {
if lhs.theme !== rhs.theme { if lhs.theme != rhs.theme {
return false return false
} }
if lhs.transcriptionState != rhs.transcriptionState { if lhs.transcriptionState != rhs.transcriptionState {
@ -40,12 +62,14 @@ public final class AudioTranscriptionButtonComponent: Component {
public final class View: UIButton { public final class View: UIButton {
private var component: AudioTranscriptionButtonComponent? private var component: AudioTranscriptionButtonComponent?
private let blurredBackgroundNode: NavigationBackgroundNode
private let backgroundLayer: SimpleLayer private let backgroundLayer: SimpleLayer
private let animationView: ComponentHostView<Empty> private let animationView: ComponentHostView<Empty>
private var progressAnimationView: ComponentHostView<Empty>? private var progressAnimationView: ComponentHostView<Empty>?
override init(frame: CGRect) { override init(frame: CGRect) {
self.blurredBackgroundNode = NavigationBackgroundNode(color: .clear)
self.backgroundLayer = SimpleLayer() self.backgroundLayer = SimpleLayer()
self.animationView = ComponentHostView<Empty>() self.animationView = ComponentHostView<Empty>()
@ -73,7 +97,22 @@ public final class AudioTranscriptionButtonComponent: Component {
func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
let size = CGSize(width: 30.0, height: 30.0) 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 { if self.component?.transcriptionState != component.transcriptionState {
switch 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.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 self.component = component

View File

@ -48,7 +48,7 @@ public final class EmojiStatusComponent: Component {
case verified(fillColor: UIColor, foregroundColor: UIColor, sizeType: SizeType) case verified(fillColor: UIColor, foregroundColor: UIColor, sizeType: SizeType)
case text(color: UIColor, string: String) case text(color: UIColor, string: String)
case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor, themeColor: UIColor?, loopMode: LoopMode) 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 public let context: AccountContext
@ -223,7 +223,7 @@ public final class EmojiStatusComponent: Component {
} else { } else {
iconImage = nil iconImage = nil
} }
case let .topic(title, colorIndex, size): case let .topic(title, color, size):
func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? { func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor]) -> UIImage? {
return generateImage(size, rotatedContext: { size, context in return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
@ -283,18 +283,21 @@ public final class EmojiStatusComponent: Component {
}) })
} }
let topicColors: [([UInt32], [UInt32])] = [ func generateTopicColors(_ color: Int32) -> ([UInt32], [UInt32]) {
([0x6FB9F0, 0x0261E4], [0x026CB5, 0x064BB7]), return ([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
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 iconImage = image
} else { } else {
iconImage = nil iconImage = nil

View File

@ -2145,7 +2145,7 @@ public final class EmojiPagerContentComponent: Component {
public enum Icon: Equatable, Hashable { public enum Icon: Equatable, Hashable {
case premiumStar case premiumStar
case topic(String) case topic(String, Int32)
} }
case animation(EntityKeyboardAnimationData) 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)) 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)) 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): case let .topic(title, color):
if let image = generateTopicIcon(backgroundColors: [UIColor(rgb: 0x6FB9F0), UIColor(rgb: 0x0261E4)], strokeColors: [UIColor(rgb: 0x026CB5), UIColor(rgb: 0x064BB7)], title: title) { 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)) 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)) 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 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) { func update(content: ItemContent) {
if self.content != content { 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 let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
if let image = generateTopicIcon(backgroundColors: [UIColor(rgb: 0x6FB9F0), UIColor(rgb: 0x0261E4)], strokeColors: [UIColor(rgb: 0x026CB5), UIColor(rgb: 0x064BB7)], title: title) { let colors = self.getTopicColors(color)
let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) 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)) 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?, chatPeerId: EnginePeer.Id?,
selectedItems: Set<MediaId> = Set(), selectedItems: Set<MediaId> = Set(),
topStatusTitle: String? = nil, topStatusTitle: String? = nil,
topicTitle: String? = nil topicTitle: String? = nil,
topicColor: Int32? = nil
) -> Signal<EmojiPagerContentComponent, NoError> { ) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -6279,7 +6295,7 @@ public final class EmojiPagerContentComponent: Component {
if isTopicIconSelection { if isTopicIconSelection {
let resultItem = EmojiPagerContentComponent.Item( let resultItem = EmojiPagerContentComponent.Item(
animationData: nil, animationData: nil,
content: .icon(.topic(String((topicTitle ?? "").prefix(1)))), content: .icon(.topic(String((topicTitle ?? "").prefix(1)), topicColor ?? 0)),
itemFile: nil, itemFile: nil,
subgroupId: nil, subgroupId: nil,
icon: .none, icon: .none,

View File

@ -23,6 +23,7 @@ private final class TitleFieldComponent: Component {
let accentColor: UIColor let accentColor: UIColor
let placeholderColor: UIColor let placeholderColor: UIColor
let fileId: Int64 let fileId: Int64
let iconColor: Int32
let text: String let text: String
let textUpdated: (String) -> Void let textUpdated: (String) -> Void
@ -32,6 +33,7 @@ private final class TitleFieldComponent: Component {
accentColor: UIColor, accentColor: UIColor,
placeholderColor: UIColor, placeholderColor: UIColor,
fileId: Int64, fileId: Int64,
iconColor: Int32,
text: String, text: String,
textUpdated: @escaping (String) -> Void textUpdated: @escaping (String) -> Void
) { ) {
@ -40,6 +42,7 @@ private final class TitleFieldComponent: Component {
self.accentColor = accentColor self.accentColor = accentColor
self.placeholderColor = placeholderColor self.placeholderColor = placeholderColor
self.fileId = fileId self.fileId = fileId
self.iconColor = iconColor
self.text = text self.text = text
self.textUpdated = textUpdated self.textUpdated = textUpdated
} }
@ -60,6 +63,9 @@ private final class TitleFieldComponent: Component {
if lhs.fileId != rhs.fileId { if lhs.fileId != rhs.fileId {
return false return false
} }
if lhs.iconColor != rhs.iconColor {
return false
}
if lhs.text != rhs.text { if lhs.text != rhs.text {
return false return false
} }
@ -106,7 +112,7 @@ private final class TitleFieldComponent: Component {
let iconContent: EmojiStatusComponent.Content let iconContent: EmojiStatusComponent.Content
if component.fileId == 0 { 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 { } 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)) 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, deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0, hiddenInputHeight: 0.0,
displayBottomPanel: false, displayBottomPanel: false,
isExpanded: false isExpanded: true
)), )),
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize
@ -347,6 +353,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
var title: String var title: String
var fileId: Int64 var fileId: Int64
var iconColor: Int32
init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void) { init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void) {
self.context = context self.context = context
@ -357,9 +364,13 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
case .create: case .create:
self.title = "" self.title = ""
self.fileId = 0 self.fileId = 0
let colors: [Int32] = [0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98,0xFF93B2, 0xFB6F5F]
self.iconColor = colors.randomElement() ?? 0x0
case let .edit(info): case let .edit(info):
self.title = info.title self.title = info.title
self.fileId = info.icon ?? 0 self.fileId = info.icon ?? 0
self.iconColor = info.iconColor
} }
super.init() super.init()
@ -378,7 +389,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId, chatPeerId: self.context.account.peerId,
selectedItems: Set(), selectedItems: Set(),
topicTitle: self.title topicTitle: self.title,
topicColor: self.iconColor
) )
|> deliverOnMainQueue).start(next: { [weak self] content in |> deliverOnMainQueue).start(next: { [weak self] content in
self?.emojiContent = content self?.emojiContent = content
@ -410,7 +422,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId, chatPeerId: self.context.account.peerId,
selectedItems: Set([MediaId(namespace: Namespaces.Media.CloudFile, id: self.fileId)]), 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 |> deliverOnMainQueue).start(next: { [weak self] content in
self?.emojiContent = content self?.emojiContent = content
@ -442,7 +455,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId, chatPeerId: self.context.account.peerId,
selectedItems: Set([MediaId(namespace: Namespaces.Media.CloudFile, id: self.fileId)]), 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 |> deliverOnMainQueue).start(next: { [weak self] content in
self?.emojiContent = content self?.emojiContent = content
@ -532,6 +546,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
accentColor: environment.theme.list.itemAccentColor, accentColor: environment.theme.list.itemAccentColor,
placeholderColor: environment.theme.list.disclosureArrowColor, placeholderColor: environment.theme.list.disclosureArrowColor,
fileId: state.fileId, fileId: state.fileId,
iconColor: state.iconColor,
text: state.title, text: state.title,
textUpdated: { [weak state] text in textUpdated: { [weak state] text in
state?.updateTitle(text) state?.updateTitle(text)
@ -568,6 +583,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
) )
contentHeight += iconHeader.size.height + headerSpacing contentHeight += iconHeader.size.height + headerSpacing
let bottomInset = max(environment.safeInsets.bottom, 12.0)
let iconBackground = iconBackground.update( let iconBackground = iconBackground.update(
component: RoundedRectangle( component: RoundedRectangle(
color: environment.theme.list.itemBlocksBackgroundColor, color: environment.theme.list.itemBlocksBackgroundColor,
@ -575,7 +592,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
), ),
availableSize: CGSize( availableSize: CGSize(
width: context.availableSize.width - sideInset * 2.0, 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 transition: context.transition
) )
@ -584,7 +601,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
) )
if let emojiContent = state.emojiContent { 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( let iconSelector = iconSelector.update(
component: TopicIconSelectionComponent( component: TopicIconSelectionComponent(
@ -592,7 +609,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
strings: environment.strings, strings: environment.strings,
deviceMetrics: environment.deviceMetrics, deviceMetrics: environment.deviceMetrics,
emojiContent: emojiContent, emojiContent: emojiContent,
backgroundColor: .clear, backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.blocksBackgroundColor separatorColor: environment.theme.list.blocksBackgroundColor
), ),
environment: {}, environment: {},
@ -601,6 +618,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
) )
context.add(iconSelector context.add(iconSelector
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + iconSelector.size.height / 2.0)) .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 let accountContext = context.component.context
@ -736,8 +755,6 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
} }
@objc private func createPressed() { @objc private func createPressed() {
// self.dismiss()
self.completion(self.state.0, self.state.1) self.completion(self.state.0, self.state.1)
} }
} }

View File

@ -733,8 +733,28 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
} }
}, forceOrientation: { orientation in }, forceOrientation: { orientation in
let value = orientation.rawValue let value = orientation.rawValue
if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
var interfaceOrientations: UIInterfaceOrientationMask = []
switch orientation {
case .portrait:
interfaceOrientations = .portrait
case .landscapeLeft:
interfaceOrientations = .landscapeLeft
case .landscapeRight:
interfaceOrientations = .landscapeRight
case .portraitUpsideDown:
interfaceOrientations = .portraitUpsideDown
case .unknown:
interfaceOrientations = .portrait
@unknown default:
interfaceOrientations = .portrait
}
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: interfaceOrientations))
} else {
UIDevice.current.setValue(value, forKey: "orientation") UIDevice.current.setValue(value, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation() UINavigationController.attemptRotationToDeviceOrientation()
}
}) })
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true) let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true)

View File

@ -4828,7 +4828,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let fileId = threadInfo.icon { 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)) 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 { } 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) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent)
} else { } else {

View File

@ -838,11 +838,11 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent,
if let fileId = self.fileId { 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)) iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 54.0, height: 54.0), placeholderColor: .clear, themeColor: serviceColor.primaryText, loopMode: .count(2))
} else { } else {
var colorIndex: Int = 0 let colorIndex: Int32 = 0
if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation { // if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation {
colorIndex = Int(clamping: abs(replyThreadMessage.effectiveTopId.id)) // colorIndex = Int(clamping: abs(replyThreadMessage.effectiveTopId.id))
} // }
iconContent = .topic(title: String(title.prefix(1)), colorIndex: colorIndex, size: CGSize(width: 64.0, height: 64.0)) 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) let insets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)

View File

@ -173,14 +173,14 @@ final class ChatMessageSwipeToReplyNode: ASDisplayNode {
var lineWidth = self.progressLayer.lineWidth var lineWidth = self.progressLayer.lineWidth
self.progressLayer.lineWidth = 1.0 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: 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.15, removeOnCompletion: false) 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 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 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.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.isHidden = false
self.fillLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath self.fillLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).cgPath

View File

@ -425,7 +425,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
if let iconFileId = threadInfo.icon { 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) content = .animation(content: .customEmoji(fileId: iconFileId), size: CGSize(width: avatarSize, height: avatarSize), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: nil, loopMode: .forever)
} else { } 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( let _ = iconView.update(
transition: .immediate, transition: .immediate,