diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index 3bd4728b38..f6dec2f877 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -220,6 +220,10 @@ public final class ResizableSheetComponent update.id { return current } + var timestamp = update.timestamp + if let current, current.id == update.id { + timestamp = current.timestamp + } return ( update.id, Namespaces.Message.Cloud, update.threadId, update.authorId, - update.timestamp, + timestamp, update.text, [ TypingDraftMessageAttribute(), diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index 04720616ac..69f123c044 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -30,6 +30,7 @@ public struct UserLimitsConfiguration: Equatable { public var maxChannelRecommendationsCount: Int32 public var maxConferenceParticipantCount: Int32 public var maxBotsCreated: Int32 + public var maxOwnedAITextStyles: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -60,7 +61,8 @@ public struct UserLimitsConfiguration: Equatable { maxGiveawayPeriodSeconds: 86400 * 31, maxChannelRecommendationsCount: 10, maxConferenceParticipantCount: 100, - maxBotsCreated: 20 + maxBotsCreated: 20, + maxOwnedAITextStyles: 5 ) } @@ -92,7 +94,8 @@ public struct UserLimitsConfiguration: Equatable { maxGiveawayPeriodSeconds: Int32, maxChannelRecommendationsCount: Int32, maxConferenceParticipantCount: Int32, - maxBotsCreated: Int32 + maxBotsCreated: Int32, + maxOwnedAITextStyles: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedSavedChatCount = maxPinnedSavedChatCount @@ -122,6 +125,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxChannelRecommendationsCount = maxChannelRecommendationsCount self.maxConferenceParticipantCount = maxConferenceParticipantCount self.maxBotsCreated = maxBotsCreated + self.maxOwnedAITextStyles = maxOwnedAITextStyles } } @@ -177,5 +181,6 @@ extension UserLimitsConfiguration { self.maxChannelRecommendationsCount = getValue("recommended_channels_limit", orElse: defaultValue.maxChannelRecommendationsCount) self.maxConferenceParticipantCount = getGeneralValue("conference_call_size_limit", orElse: defaultValue.maxConferenceParticipantCount) self.maxBotsCreated = getValue("bots_create_limit", orElse: defaultValue.maxBotsCreated) + self.maxOwnedAITextStyles = getValue("aicompose_tone_saved_limit", orElse: defaultValue.maxBotsCreated) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index d1a13204ad..3991971896 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -64,6 +64,7 @@ public enum EngineConfiguration { public let maxChannelRecommendationsCount: Int32 public let maxConferenceParticipantCount: Int32 public let maxBotsCreated: Int32 + public let maxOwnedAITextStyles: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -97,7 +98,8 @@ public enum EngineConfiguration { maxGiveawayPeriodSeconds: Int32, maxChannelRecommendationsCount: Int32, maxConferenceParticipantCount: Int32, - maxBotsCreated: Int32 + maxBotsCreated: Int32, + maxOwnedAITextStyles: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedSavedChatCount = maxPinnedSavedChatCount @@ -127,6 +129,7 @@ public enum EngineConfiguration { self.maxChannelRecommendationsCount = maxChannelRecommendationsCount self.maxConferenceParticipantCount = maxConferenceParticipantCount self.maxBotsCreated = maxBotsCreated + self.maxOwnedAITextStyles = maxOwnedAITextStyles } } } @@ -191,7 +194,8 @@ public extension EngineConfiguration.UserLimits { maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds, maxChannelRecommendationsCount: userLimitsConfiguration.maxChannelRecommendationsCount, maxConferenceParticipantCount: userLimitsConfiguration.maxConferenceParticipantCount, - maxBotsCreated: userLimitsConfiguration.maxBotsCreated + maxBotsCreated: userLimitsConfiguration.maxBotsCreated, + maxOwnedAITextStyles: userLimitsConfiguration.maxOwnedAITextStyles ) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index a03fc2eba8..379bac9399 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -798,7 +798,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width - if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue, !hasDraft { suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0) } let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right @@ -810,7 +810,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) boundingSize = textFrameWithoutInsets.size - if let statusSizeAndApply = statusSizeAndApply { + if let statusSizeAndApply = statusSizeAndApply, !hasDraft { boundingSize.height += statusSizeAndApply.0.height } diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index f66d46fa98..6d0a5d7c57 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -1106,6 +1106,9 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { titleViewTransition.updateFrame(view: titleView, frame: titleFrame) } else { + leftTitleInset = max(leftTitleInset, rightTitleInset) + rightTitleInset = leftTitleInset + let titleSize = CGSize(width: max(1.0, size.width - leftTitleInset - rightTitleInset), height: nominalHeight) var titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) if titleFrame.origin.x + titleFrame.width > size.width - rightTitleInset { diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift index bc1fbb70b5..2d856b6e6f 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift @@ -285,21 +285,39 @@ final class TextProcessingContentComponent: Component { return } - environment.controller()?.push(textAlertController( - context: component.context, - title: "Delete Style", - text: "Are you sure you want to delete this style? It will be removed for everyone who installed it.", - actions: [ - TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), - TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in - guard let self, let component = self.component else { - return - } - let _ = component.context.engine.messages.deleteAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone() - component.styleDeleted(id.id) - }), - ] - )) + if style.isCreator { + environment.controller()?.push(textAlertController( + context: component.context, + title: "Delete Style", + text: "Are you sure you want to delete this style? It will be removed for everyone who installed it.", + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.deleteAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone() + component.styleDeleted(id.id) + }), + ] + )) + } else { + environment.controller()?.push(textAlertController( + context: component.context, + title: "Delete Style", + text: "Are you sure you want to delete this style?", + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.unsaveAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone() + component.styleDeleted(id.id) + }), + ] + )) + } } private func openStyleContextMenu(id: TelegramComposeAIMessageMode.StyleReference, gesture: ContextGesture, sourceView: ContextExtractedContentContainingView) { @@ -749,6 +767,53 @@ final class TextProcessingContentComponent: Component { guard let component = self.component, let environment = self.environment else { return } + + let hasPremium = await (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) + ) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + }).get() + let userLimits = await component.context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: hasPremium) + ).get() + + let maxStyles = Int(userLimits.maxOwnedAITextStyles) + + if component.styles.filter({ $0.isAuthor }).count >= maxStyles { + if !hasPremium { + //TODO:localize + let context = component.context + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .aiTools, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesStealthMode, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak self, weak controller] c in + controller?.dismiss(animated: true, completion: { + guard let self else { + return + } + self.environment?.controller()?.push(c) + }) + } + environment.controller()?.push(controller) + } else { + environment.controller()?.push(textAlertController( + context: component.context, + title: "Too Many Styles", + text: "Please delete some of your saved styles to create a new one.", + actions: [ + TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {}), + ] + )) + } + return + } + environment.controller()?.push(await TextStyleEditScreen( context: component.context, mode: .create, @@ -1101,7 +1166,6 @@ private final class TextProcessingSheetComponent: Component { final class View: UIView { private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() private var toast: ComponentView? - private var languageSelectionMenu: ComponentView? private let animateOut = ActionSlot>() private let contentExternalState = TextProcessingContentComponent.ExternalState() @@ -1123,6 +1187,10 @@ private final class TextProcessingSheetComponent: Component { } } private var languageSelectionMenuData: LanguageSelectionMenuData? + private var languageSelectionMenu: ComponentView? + + private var styleCreatedToastData: (timer: Foundation.Timer, emojiFile: TelegramMediaFile, style: TelegramComposeAIMessageMode.CloudStyle.Custom)? + private var styleCreatedToast: ComponentView? private var component: TextProcessingSheetComponent? private var environment: ViewControllerComponentContainer.Environment? @@ -1142,6 +1210,7 @@ private final class TextProcessingSheetComponent: Component { deinit { self.actionDisposable?.dispose() + self.styleCreatedToastData?.timer.invalidate() } private func displayLongPressSendMenu(sourceSendButton: UIView) { @@ -1433,6 +1502,21 @@ private final class TextProcessingSheetComponent: Component { TelegramEngine.EngineData.Item.Peer.Peer(id: authorId) ).get() } + + if case let .custom(style) = style.content, let emojiFileId = style.emojiFileId { + let emojiFile = await component.context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get().first?.value + + if let emojiFile { + self.styleCreatedToastData = (Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.styleCreatedToastData = nil + self.state?.updated(transition: .spring(duration: 0.4)) + }), emojiFile, style) + } + } + self.styles.insert(TextProcessingScreen.Style(cloudStyle: style, authorPeer: authorPeer), at: 0) self.state?.updated(transition: .spring(duration: 0.4)) } @@ -1636,6 +1720,73 @@ private final class TextProcessingSheetComponent: Component { } } } + + if let styleCreatedToastData = self.styleCreatedToastData, !self.contentExternalState.nonPremiumFloodTriggered { + let sideInset: CGFloat = 8.0 + + let styleCreatedToast: ComponentView + var styleCreatedToastTransition = transition + if let current = self.styleCreatedToast { + styleCreatedToast = current + } else { + styleCreatedToastTransition = styleCreatedToastTransition.withAnimation(.none) + styleCreatedToast = ComponentView() + self.styleCreatedToast = styleCreatedToast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let styleCreatedToastSize = styleCreatedToast.update( + transition: styleCreatedToastTransition, + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .file(file: styleCreatedToastData.emojiFile), + size: CGSize(width: 32.0, height: 32.0), + placeholderColor: environmentValue.theme.list.mediaPlaceholderColor, + themeColor: environmentValue.theme.list.itemPrimaryTextColor, + loopMode: .count(1) + ), + size: CGSize(width: 32.0, height: 32.0), + isVisibleForAnimations: true, + action: nil + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(styleCreatedToastData.style.title) style created!", font: Font.semibold(14.0), textColor: .white)), + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: "Press and hold a style to edit or share the link.", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let styleCreatedToastView = styleCreatedToast.view { + if styleCreatedToastView.superview == nil, let sheetView = self.sheet.view as? ResizableSheetComponent.View { + sheetView.containerView.addSubview(styleCreatedToastView) + if !transition.animation.isImmediate { + styleCreatedToastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + styleCreatedToastTransition.setFrame(view: styleCreatedToastView, frame: CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - 94.0 - styleCreatedToastSize.height), size: styleCreatedToastSize)) + } + } else { + if let styleCreatedToast = self.styleCreatedToast { + self.styleCreatedToast = nil + if let styleCreatedToastView = styleCreatedToast.view { + styleCreatedToastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak styleCreatedToastView] _ in + styleCreatedToastView?.removeFromSuperview() + }) + } + } + } if let languageSelectionMenuDataValue = self.languageSelectionMenuData { let languageSelectionMenu: ComponentView diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift index 9e937b4d92..073a9b0978 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift @@ -152,6 +152,53 @@ final class TextStyleEditContentComponent: Component { } } } + + func activateEmojiSelection() { + guard let component = self.component else { + return + } + guard let iconBackgroundView = self.iconBackground.view else { + return + } + self.environment?.controller()?.present(component.context.sharedContext.makeEmojiStatusSelectionController( + context: component.context, + mode: .backgroundSelection(completion: { [weak self] file in + guard let self, let component = self.component else { + return + } + component.externalState.emojiFile = file + self.state?.updated(transition: .immediate) + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + } + }), + sourceView: iconBackgroundView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + isStandalone: false, + subject: .emoji, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: component.context.account.peerId, + selectedItems: Set() + ) |> map { $0 }, + currentSelection: nil, + color: nil, + destinationItemView: { [weak self] in + guard let self else { + return nil + } + return self.emojiIcon?.view + } + ), in: .window(.root)) + } func update(component: TextStyleEditContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -195,50 +242,10 @@ final class TextStyleEditContentComponent: Component { smoothCorners: false )), action: { [weak self] in - guard let self, let component = self.component else { + guard let self else { return } - guard let iconBackgroundView = self.iconBackground.view else { - return - } - self.environment?.controller()?.present(component.context.sharedContext.makeEmojiStatusSelectionController( - context: component.context, - mode: .backgroundSelection(completion: { [weak self] file in - guard let self, let component = self.component else { - return - } - component.externalState.emojiFile = file - self.state?.updated(transition: .immediate) - DispatchQueue.main.async { [weak self] in - guard let self else { - return - } - self.state?.updated(transition: .immediate) - } - }), - sourceView: iconBackgroundView, - emojiContent: EmojiPagerContentComponent.emojiInputData( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - isStandalone: false, - subject: .emoji, - hasTrending: false, - topReactionItems: [], - areUnicodeEmojiEnabled: false, - areCustomEmojiEnabled: true, - chatPeerId: component.context.account.peerId, - selectedItems: Set() - ) |> map { $0 }, - currentSelection: nil, - color: nil, - destinationItemView: { [weak self] in - guard let self else { - return nil - } - return self.emojiIcon?.view - } - ), in: .window(.root)) + self.activateEmojiSelection() }, animateAlpha: false, animateScale: false, @@ -338,9 +345,9 @@ final class TextStyleEditContentComponent: Component { return ListMultilineTextFieldItemComponent.ResetText(value: resetTitle) }, placeholder: "Style Name (for example, \"Pirate\")", - autocapitalizationType: .none, - autocorrectionType: .no, - characterLimit: 256, + autocapitalizationType: .sentences, + autocorrectionType: .default, + characterLimit: 12, emptyLineHandling: .notAllowed, updated: nil, textUpdateTransition: .spring(duration: 0.4), @@ -383,9 +390,9 @@ final class TextStyleEditContentComponent: Component { }, placeholder: "Instructions (for example, \"Write like a swashbuckling pirate. Use arr, ye, matey, and talk about treasure, the sea, and rum\")", placeholderDefinesMinHeight: true, - autocapitalizationType: .none, - autocorrectionType: .no, - characterLimit: 4096, + autocapitalizationType: .sentences, + autocorrectionType: .default, + characterLimit: 1024, emptyLineHandling: .allowed, updated: nil, textUpdateTransition: .spring(duration: 0.4), @@ -615,6 +622,11 @@ private final class TextStyleEditSheetComponent: Component { return } guard let emojiFile = self.contentState.emojiFile else { + if let sheetView = self.sheet.view as? ResizableSheetComponent.View { + if let contentView = sheetView.contentViewValue as? TextStyleEditContentComponent.View { + contentView.activateEmojiSelection() + } + } return } @@ -731,7 +743,7 @@ private final class TextStyleEditSheetComponent: Component { } self.performCreateStyle() } - let isMainActionEnabled = !self.contentState.titleInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !self.contentState.textInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.contentState.emojiFile != nil && !self.isActionInProgress + let isMainActionEnabled = !self.contentState.titleInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !self.contentState.textInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !self.isActionInProgress let actionButtonTitle: String let titleString: String switch component.mode { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b262549202..a36e3ea1f1 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1177,6 +1177,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH } self.isInteractivelyScrollingValue = true self.isInteractivelyScrollingPromise.set(true) + //self.pinToTopStableId = nil self.beganDragging?() } @@ -2150,7 +2151,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH if message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { for j in (0 ..< i).reversed() { let otherMessage = view.entries[j].message - if !otherMessage.effectivelyIncoming(context.account.peerId) { + if !otherMessage.effectivelyIncoming(context.account.peerId) && otherMessage.id.namespace == Namespaces.Message.Cloud { if strongSelf.pinToTopStableId != otherMessage.stableId { strongSelf.pinToTopStableId = otherMessage.stableId scrollToPinToTopStableId = true