Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Mikhail Filimonov 2025-05-23 08:13:47 +01:00
commit d1e1fffed9
41 changed files with 1180 additions and 538 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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? {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

@ -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 {

View File

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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {