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