diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2f3ea5fe2b..b8b3f1f7f8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9863,6 +9863,8 @@ Sorry for the inconvenience."; "Story.ViewList.ViewerCount_any" = "%d Viewers"; "AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@."; +"Notification.BotWriteAllowedMenu" = "You allowed this bot to message you when you added it to your attachment menu."; +"Notification.BotWriteAllowedRequest" = "You allowed this bot to message you in the app."; "Story.Privacy.PostStoryAs" = "Post Story As"; "Story.Privacy.PostStoryAsHeader" = "POST STORY AS"; @@ -9870,3 +9872,32 @@ Sorry for the inconvenience."; "Story.Privacy.KeepOnChannelPageInfo" = "Keep this story on channel profile even after it expires in %@."; "Story.Editor.TooltipPremiumReaction" = "Subscribe to [Telegram Premium]() to use this reaction."; + +"Story.Privacy.TooltipStoryArchivedChannel" = "Users will see this story on the channel page even after it expires."; + +"Story.Editor.TooltipMutedWithAudio" = "Original audio will be removed"; +"Story.Editor.TooltipUnmutedWithAudio" = "Original audio will be preserved"; + +"SecretImage.ViewOnce.Title" = "Disappearing Photo"; +"SecretVideo.ViewOnce.Title" = "Disappearing Video"; + +"MediaPicker.Timer.Description" = "Choose how long the media will be kept after opening."; +"MediaPicker.Timer.ViewOnce" = "View Once"; +"MediaPicker.Timer.Seconds_1" = "%d Second"; +"MediaPicker.Timer.Seconds_any" = "%d Seconds"; +"MediaPicker.Timer.DoNotDelete" = "Do Not Delete"; + +"MediaPicker.Timer.Photo.ViewOnceTooltip" = "Photo set to view once."; +"MediaPicker.Timer.Photo.TimerTooltip" = "Photo will be deleted in\n%@ seconds after opening."; +"MediaPicker.Timer.Photo.KeepTooltip" = "Photo will be kept in chat."; + +"MediaPicker.Timer.Video.ViewOnceTooltip" = "Video set to view once."; +"MediaPicker.Timer.Video.TimerTooltip" = "Video will be deleted in\n%@ seconds after opening."; +"MediaPicker.Timer.Video.KeepTooltip" = "Video will be kept in chat."; + +"WebApp.AllowWriteTitle" = "Allow Sending Messages?"; +"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram."; + +"WebApp.SharePhoneTitle" = "Share Phone Number?"; +"WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services."; +"WebApp.SharePhoneConfirmationUnblock" = "**%@** will know your phone number. This can be useful for integration with other services.\n\nThis will also unblock the bot."; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 9faeb8ac77..015052f58a 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -941,7 +941,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.chatListDisplayNode.mainContainerNode.present = { [weak self] c in if let strongSelf = self { - strongSelf.present(c, in: .window(.root)) + if c is UndoOverlayController { + strongSelf.dismissAllUndoControllers() + strongSelf.present(c, in: .current) + } else { + strongSelf.present(c, in: .window(.root)) + } } } @@ -3118,8 +3123,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - public func scrollToStories() { + public func scrollToStories(peerId: EnginePeer.Id? = nil) { self.chatListDisplayNode.scrollToStories(animated: false) + + if let peerId, let componentView = self.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() { + storyPeerListView.ensureItemVisible(peerId: peerId) + } } public func scrollToStoriesAnimated() { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index b370cded08..d69303cd59 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -192,7 +192,7 @@ private final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {}, performActiveSessionAction: { _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 68e3e61e6c..dadb6d7ec4 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -439,6 +439,13 @@ private final class ItemNode: ASDisplayNode { return deleteButtonNode.view } } + + if self.buttonNode.isUserInteractionEnabled { + if let result = self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event) { + return result + } + } + return super.hitTest(point, with: event) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 8dc2cd66fa..12001f3652 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2216,6 +2216,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openActiveSessions: { + }, performActiveSessionAction: { _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { [weak self] subject, sourceNode in @@ -3533,7 +3535,9 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, performActiveSessionAction: { _ in + }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }) var isInlineMode = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index bc55fda784..9cc5fa5f33 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -20,6 +20,7 @@ import Postbox import ChatFolderLinkPreviewScreen import StoryContainerScreen import ChatListHeaderComponent +import UndoUI public enum ChatListNodeMode { case chatList(appendContacts: Bool) @@ -98,6 +99,8 @@ public final class ChatListNodeInteraction { let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void + let openActiveSessions: () -> Void + let performActiveSessionAction: (Bool) -> Void let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void @@ -146,6 +149,8 @@ public final class ChatListNodeInteraction { openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, + openActiveSessions: @escaping () -> Void, + performActiveSessionAction: @escaping (Bool) -> Void, openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void @@ -181,6 +186,8 @@ public final class ChatListNodeInteraction { self.openStorageManagement = openStorageManagement self.openPasswordSetup = openPasswordSetup self.openPremiumIntro = openPremiumIntro + self.openActiveSessions = openActiveSessions + self.performActiveSessionAction = performActiveSessionAction self.openChatFolderUpdates = openChatFolderUpdates self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories @@ -700,12 +707,21 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() + case .reviewLogin: + break } case .hide: switch notice { default: break } + case let .buttonChoice(isPositive): + switch notice { + case .reviewLogin: + nodeInteraction?.performActiveSessionAction(isPositive) + default: + break + } } }), directionHint: entry.directionHint) } @@ -1011,12 +1027,21 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() + case .reviewLogin: + break } case .hide: switch notice { default: break } + case let .buttonChoice(isPositive): + switch notice { + case .reviewLogin: + nodeInteraction?.performActiveSessionAction(isPositive) + default: + break + } } }), directionHint: entry.directionHint) case .HeaderEntry: @@ -1564,6 +1589,53 @@ public final class ChatListNode: ListView { } let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) self.push?(controller) + }, openActiveSessions: { [weak self] in + guard let self else { + return + } + + let activeSessionsContext = self.context.engine.privacy.activeSessions() + let _ = (activeSessionsContext.state + |> filter { state in + return !state.sessions.isEmpty + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + + let recentSessionsController = self.context.sharedContext.makeRecentSessionsController(context: self.context, activeSessionsContext: activeSessionsContext) + self.push?(recentSessionsController) + }) + }, performActiveSessionAction: { [weak self] isPositive in + guard let self else { + return + } + + if isPositive { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let animationBackgroundColor: UIColor + if presentationData.theme.overallDarkAppearance { + animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + } else { + animationBackgroundColor = UIColor(rgb: 0x474747) + } + //TODO:localize + self.present?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: "New Login Allowed", text: "You can check the list of your active logins in [Settings > Devices]().", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { [weak self] action in + switch action { + case .info: + self?.interaction?.openActiveSessions() + default: + break + } + + return true + })) + } else { + + } }, openChatFolderUpdates: { [weak self] in guard let self else { return @@ -1718,6 +1790,11 @@ public final class ChatListNode: ListView { } } } else { + #if DEBUG + if "".isEmpty { + return .single(.reviewLogin(device: "Macbook M2", location: "Stockholm, Sweden")) + } + #endif return .single(nil) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index bf851892c7..c90df507bb 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -85,6 +85,7 @@ enum ChatListNotice: Equatable { case premiumUpgrade(discount: Int32) case premiumAnnualDiscount(discount: Int32) case premiumRestore(discount: Int32) + case reviewLogin(device: String, location: String) } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index c2af5ee707..ebe5e03003 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -12,6 +12,7 @@ class ChatListStorageInfoItem: ListViewItem { enum Action { case activate case hide + case buttonChoice(isPositive: Bool) } let theme: PresentationTheme @@ -84,6 +85,11 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { private let arrowNode: ASImageNode private let separatorNode: ASDisplayNode + private var okButtonText: TextNode? + private var cancelButtonText: TextNode? + private var okButton: HighlightableButtonNode? + private var cancelButton: HighlightableButtonNode? + private var item: ChatListStorageInfoItem? required init() { @@ -124,20 +130,27 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeOkButtonTextLayout = TextNode.asyncLayout(self.okButtonText) + let makeCancelButtonTextLayout = TextNode.asyncLayout(self.cancelButtonText) + return { item, params, last in let baseWidth = params.width - params.leftInset - params.rightInset let _ = baseWidth let sideInset: CGFloat = params.leftInset + 16.0 let rightInset: CGFloat = sideInset + 24.0 - let verticalInset: CGFloat = 8.0 - let spacing: CGFloat = 0.0 + let verticalInset: CGFloat = 9.0 + var spacing: CGFloat = 0.0 let themeUpdated = item.theme !== previousItem?.theme let titleString: NSAttributedString let textString: NSAttributedString + var okButtonLayout: (TextNodeLayout, () -> TextNode)? + var cancelButtonLayout: (TextNodeLayout, () -> TextNode)? + var alignment: NSTextAlignment = .left + switch item.notice { case let .clearStorage(sizeFraction): let sizeString = dataSizeString(Int64(sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: ".")) @@ -182,13 +195,30 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { titleString = titleStringValue textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + case let .reviewLogin(device, location): + spacing = 2.0 + alignment = .center + + //TODO:localize + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: "Someone just got acess to your messages!", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + titleString = titleStringValue + + textString = NSAttributedString(string: "We detected a new login to your account from \(device), \(location). Is it you?", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + + okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Yes, it's me", font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No, it's not me!", font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) } - let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18)) - let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 10, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18)) - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height), insets: UIEdgeInsets()) + var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height) + if let okButtonLayout { + contentSize.height += okButtonLayout.0.size.height + 20.0 + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) return (layout, { [weak self] in if let strongSelf = self { @@ -203,15 +233,94 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)) let _ = titleLayout.1() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleLayout.0.size) + if case .center = alignment { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size) + } else { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleLayout.0.size) + } let _ = textLayout.1() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) + + if case .center = alignment { + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) + } else { + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) + } if let image = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) } + if let okButtonLayout, let cancelButtonLayout { + strongSelf.arrowNode.isHidden = true + + let okButton: HighlightableButtonNode + if let current = strongSelf.okButton { + okButton = current + } else { + okButton = HighlightableButtonNode() + strongSelf.okButton = okButton + strongSelf.addSubnode(okButton) + okButton.addTarget(strongSelf, action: #selector(strongSelf.okButtonPressed), forControlEvents: .touchUpInside) + } + + let cancelButton: HighlightableButtonNode + if let current = strongSelf.cancelButton { + cancelButton = current + } else { + cancelButton = HighlightableButtonNode() + strongSelf.cancelButton = cancelButton + strongSelf.addSubnode(cancelButton) + cancelButton.addTarget(strongSelf, action: #selector(strongSelf.cancelButtonPressed), forControlEvents: .touchUpInside) + } + + let okButtonText = okButtonLayout.1() + if okButtonText !== strongSelf.okButtonText { + strongSelf.okButtonText?.removeFromSupernode() + strongSelf.okButtonText = okButtonText + okButton.addSubnode(okButtonText) + } + + let cancelButtonText = cancelButtonLayout.1() + if cancelButtonText !== strongSelf.okButtonText { + strongSelf.cancelButtonText?.removeFromSupernode() + strongSelf.cancelButtonText = cancelButtonText + cancelButton.addSubnode(cancelButtonText) + } + + let buttonsWidth: CGFloat = max(min(300.0, params.width), okButtonLayout.0.size.width + cancelButtonLayout.0.size.width + 32.0) + let buttonWidth: CGFloat = floor(buttonsWidth * 0.5) + let buttonHeight: CGFloat = 32.0 + + let okButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - buttonsWidth) * 0.5), y: strongSelf.textNode.frame.maxY + 6.0), size: CGSize(width: buttonWidth, height: buttonHeight)) + let cancelButtonFrame = CGRect(origin: CGPoint(x: okButtonFrame.maxX, y: strongSelf.textNode.frame.maxY + 6.0), size: CGSize(width: buttonWidth, height: buttonHeight)) + + okButton.frame = okButtonFrame + cancelButton.frame = cancelButtonFrame + + okButtonText.frame = CGRect(origin: CGPoint(x: floor((okButtonFrame.width - okButtonLayout.0.size.width) * 0.5), y: floor((okButtonFrame.height - okButtonLayout.0.size.height) * 0.5)), size: okButtonLayout.0.size) + cancelButtonText.frame = CGRect(origin: CGPoint(x: floor((cancelButtonFrame.width - cancelButtonLayout.0.size.width) * 0.5), y: floor((cancelButtonFrame.height - cancelButtonLayout.0.size.height) * 0.5)), size: cancelButtonLayout.0.size) + } else { + strongSelf.arrowNode.isHidden = false + + if let okButton = strongSelf.okButton { + strongSelf.okButton = nil + okButton.removeFromSupernode() + } + if let cancelButton = strongSelf.cancelButton { + strongSelf.cancelButton = nil + cancelButton.removeFromSupernode() + } + if let okButtonText = strongSelf.okButtonText { + strongSelf.okButtonText = nil + okButtonText.removeFromSupernode() + } + if let cancelButtonText = strongSelf.cancelButtonText { + strongSelf.cancelButtonText = nil + cancelButtonText.removeFromSupernode() + } + } + strongSelf.contentSize = layout.contentSize strongSelf.insets = layout.insets @@ -228,6 +337,14 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { } } + @objc private func okButtonPressed() { + self.item?.action(.buttonChoice(isPositive: true)) + } + + @objc private func cancelButtonPressed() { + self.item?.action(.buttonChoice(isPositive: false)) + } + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index c31b1e5969..f0ccd4f1d1 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -392,11 +392,15 @@ public final class DrawingStickerEntityView: DrawingEntityView { } override func onSelection() { - self.presentReactionSelection() + if self.isReaction { + self.presentReactionSelection() + } } func onDeselection() { - let _ = self.dismissReactionSelection() + if self.isReaction { + let _ = self.dismissReactionSelection() + } } private weak var reactionContextNode: ReactionContextNode? diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index ab0adbfdff..675f3aecf5 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -880,6 +880,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll displayCaption = !self.textNode.isHidden } + if metrics.isTablet { + self.fullscreenButton.isHidden = true + } + var textFrame = CGRect() var visibleTextHeight: CGFloat = 0.0 if !self.textNode.isHidden { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index baee287134..9c6e0295f5 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -184,6 +184,7 @@ public func galleryItemForEntry( location: location, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected, + isSecret: isSecret, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, @@ -265,6 +266,7 @@ public func galleryItemForEntry( location: location, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected, + isSecret: isSecret, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index f6ca4b29c5..3cfe8427e6 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -115,18 +115,20 @@ class ChatImageGalleryItem: GalleryItem { let location: MessageHistoryEntryLocation? let translateToLanguage: String? let peerIsCopyProtected: Bool + let isSecret: Bool let displayInfoOnTop: Bool let performAction: (GalleryControllerInteractionTapAction) -> Void let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void let present: (ViewController, Any?) -> Void - init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData self.message = message self.location = location self.translateToLanguage = translateToLanguage self.peerIsCopyProtected = peerIsCopyProtected + self.isSecret = isSecret self.displayInfoOnTop = displayInfoOnTop self.performAction = performAction self.openActionOptions = openActionOptions @@ -136,7 +138,7 @@ class ChatImageGalleryItem: GalleryItem { func node(synchronous: Bool) -> GalleryItemNode { let node = ChatImageGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present) - node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected) + node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret) for media in self.message.media { if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage { node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) @@ -175,7 +177,7 @@ class ChatImageGalleryItem: GalleryItem { if self.displayInfoOnTop { node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) } - node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected) + node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret) } } @@ -204,6 +206,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private var message: Message? private var translateToLanguage: String? private var peerIsCopyProtected: Bool = false + private var isSecret: Bool = false private let presentationData: PresentationData private let imageNode: TransformImageNode @@ -328,11 +331,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) } - fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool) { + fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) { self.message = message self.translateToLanguage = translateToLanguage self.peerIsCopyProtected = peerIsCopyProtected - self.imageNode.captureProtected = message.isCopyProtected() + self.isSecret = isSecret + self.imageNode.captureProtected = message.isCopyProtected() || peerIsCopyProtected || isSecret self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected) } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 26a0fddda9..2a72ee37c4 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -60,7 +60,7 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { var beginTimeAndTimeout: (Double, Double)? { didSet { - if let (beginTime, timeout) = self.beginTimeAndTimeout { + if let (beginTime, timeout) = self.beginTimeAndTimeout, Int32(timeout) != viewOnceTimeout { if self.timeoutNode == nil { let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.timeoutNode = timeoutNode @@ -139,6 +139,7 @@ public final class SecretMediaPreviewController: ViewController { private var messageView: MessageView? private var currentNodeMessageId: MessageId? private var currentNodeMessageIsVideo = false + private var currentNodeMessageIsViewOnce = false private var tempFile: TempBoxFile? private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) @@ -263,6 +264,8 @@ public final class SecretMediaPreviewController: ViewController { } if let attribute = message.autoclearAttribute { + strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout + if let countdownBeginTime = attribute.countdownBeginTime { if let videoDuration = videoDuration { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) @@ -271,6 +274,8 @@ public final class SecretMediaPreviewController: ViewController { } } } else if let attribute = message.autoremoveAttribute { + strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout + if let countdownBeginTime = attribute.countdownBeginTime { if let videoDuration = videoDuration { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) @@ -284,10 +289,18 @@ public final class SecretMediaPreviewController: ViewController { if file.isAnimated { strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title } else { - strongSelf.title = strongSelf.presentationData.strings.SecretVideo_Title + if strongSelf.currentNodeMessageIsViewOnce { + strongSelf.title = strongSelf.presentationData.strings.SecretVideo_ViewOnce_Title + } else { + strongSelf.title = strongSelf.presentationData.strings.SecretVideo_Title + } } } else { - strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title + if strongSelf.currentNodeMessageIsViewOnce { + strongSelf.title = strongSelf.presentationData.strings.SecretImage_ViewOnce_Title + } else { + strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title + } } if let beginTimeAndTimeout = beginTimeAndTimeout { @@ -478,7 +491,7 @@ public final class SecretMediaPreviewController: ViewController { if !self.didSetReady { self._ready.set(.single(true)) } - if !self.currentNodeMessageIsVideo { + if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) { self.dismiss() } } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 8ce8b90239..ea08245cfa 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -95,6 +95,8 @@ public final class HashtagSearchController: TelegramBaseController { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openActiveSessions: { + }, performActiveSessionAction: { _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift index 592b6590bc..4a3de19f0c 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift @@ -63,7 +63,7 @@ public enum LegacyICloudFilePickerMode { } } -public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudFilePickerMode = .default, documentTypes: [String] = ["public.item"], forceDarkTheme: Bool = false, completion: @escaping ([URL]) -> Void) -> ViewController { +public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudFilePickerMode = .default, documentTypes: [String] = ["public.item"], forceDarkTheme: Bool = false, dismissed: @escaping () -> Void = {}, completion: @escaping ([URL]) -> Void) -> ViewController { var dismissImpl: (() -> Void)? let legacyController = LegacyICloudFileController(presentation: .modal(animateIn: true), theme: theme, completion: { urls in dismissImpl?() @@ -96,6 +96,7 @@ public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudF if let legacyController = legacyController { legacyController.dismiss() } + dismissed() } legacyController.bind(controller: UIViewController()) return legacyController diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 5317fa1122..c557339e3a 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -412,6 +412,7 @@ private final class LocationPickerContext: AttachmentMediaPickerContext { public func storyLocationPickerController( context: AccountContext, location: CLLocationCoordinate2D?, + dismissed: @escaping () -> Void, completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) @@ -427,5 +428,8 @@ public func storyLocationPickerController( } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + controller.didDismiss = { + dismissed() + } return controller } diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index f790cf06d9..8c424477d7 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -68,7 +68,6 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { super.init() self.isOpaque = false -// self.isLayerBacked = true class DisplayLinkProxy: NSObject { weak var target: RadialStatusSecretTimeoutContentNode? @@ -85,7 +84,9 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { self.displayLink?.isPaused = true self.displayLink?.add(to: RunLoop.main, forMode: .common) - self.addSubnode(self.animationNode) + if icon != nil { + self.addSubnode(self.animationNode) + } } deinit { @@ -95,7 +96,14 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { override func layout() { super.layout() - self.animationNode.frame = CGRect(x: 6.0, y: 2.0, width: 36.0, height: 36.0) + var factor: CGFloat = 0.75 + var offset: CGFloat = 0.0415 + if self.bounds.width < 30.0 { + factor = 0.66 + offset = 0.08 + } + let size = floorToScreenPixels(self.bounds.width * factor) + self.animationNode.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size) } override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) { @@ -194,15 +202,15 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters { - if let icon = parameters.icon, let iconImage = icon.cgImage { - let imageRect = CGRect(origin: CGPoint(x: floor((bounds.size.width - icon.size.width) / 2.0), y: floor((bounds.size.height - icon.size.height) / 2.0)), size: icon.size) - context.saveGState() - context.translateBy(x: imageRect.midX, y: imageRect.midY) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -imageRect.midX, y: -imageRect.midY) - context.draw(iconImage, in: imageRect) - context.restoreGState() - } +// if let icon = parameters.icon, let _ = icon.cgImage { +// let imageRect = CGRect(origin: CGPoint(x: floor((bounds.size.width - icon.size.width) / 2.0), y: floor((bounds.size.height - icon.size.height) / 2.0)), size: icon.size) +// context.saveGState() +// context.translateBy(x: imageRect.midX, y: imageRect.midY) +// context.scaleBy(x: 1.0, y: -1.0) +// context.translateBy(x: -imageRect.midX, y: -imageRect.midY) +// context.draw(iconImage, in: imageRect) +// context.restoreGState() +// } let lineWidth: CGFloat if parameters.sparks { diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 6980923433..3fd9892890 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,9 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, performActiveSessionAction: { _ in + }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index bff04bcf23..492bb27fab 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -856,7 +856,9 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, performActiveSessionAction: { _ in + }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index ccf1581433..cb1c317edd 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -370,7 +370,9 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openChatFolderUpdates: {}, hideChatFolderUpdates: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, performActiveSessionAction: { _ in + }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index f2e50f94a0..ef208b69f4 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -820,7 +820,7 @@ public final class SparseItemGrid: ASDisplayNode { self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) } - func ensureItemVisible(index: Int) { + func ensureItemVisible(index: Int, anyAmount: Bool) { guard let layout = self.layout, let _ = self.items else { return } @@ -830,8 +830,14 @@ public final class SparseItemGrid: ASDisplayNode { let itemFrame = layout.frame(at: index) let visibleBounds = self.scrollView.bounds - if itemFrame.intersects(visibleBounds) { - return + if anyAmount { + if itemFrame.intersects(visibleBounds) { + return + } + } else { + if visibleBounds.contains(itemFrame) { + return + } } var contentOffset: CGFloat @@ -1936,11 +1942,11 @@ public final class SparseItemGrid: ASDisplayNode { currentViewport.scrollToItem(at: index) } - public func ensureItemVisible(index: Int) { + public func ensureItemVisible(index: Int, anyAmount: Bool = true) { guard let currentViewport = self.currentViewport else { return } - currentViewport.ensureItemVisible(index: index) + currentViewport.ensureItemVisible(index: index, anyAmount: anyAmount) } public func scrollToTop() -> Bool { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index c5dcca430b..5c7fa0dd06 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -804,7 +804,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } dict[-1329730875] = { return Api.StoryView.parse_storyView($0) } - dict[-968094825] = { return Api.StoryViews.parse_storyViews($0) } + dict[-1923523370] = { return Api.StoryViews.parse_storyViews($0) } dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 2c9d4c7da7..907f473b69 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -610,17 +610,23 @@ public extension Api { } public extension Api { enum StoryViews: TypeConstructorDescription { - case storyViews(flags: Int32, viewsCount: Int32, reactionsCount: Int32, recentViewers: [Int64]?) + case storyViews(flags: Int32, viewsCount: Int32, forwardsCount: Int32?, reactions: [Api.ReactionCount]?, reactionsCount: Int32?, recentViewers: [Int64]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers): + case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): if boxed { - buffer.appendInt32(-968094825) + buffer.appendInt32(-1923523370) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(viewsCount, buffer: buffer, boxed: false) - serializeInt32(reactionsCount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(forwardsCount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions!.count)) + for item in reactions! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(reactionsCount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) buffer.appendInt32(Int32(recentViewers!.count)) for item in recentViewers! { @@ -632,8 +638,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViews(let flags, let viewsCount, let reactionsCount, let recentViewers): - return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) + case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): + return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactions", reactions as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) } } @@ -643,17 +649,25 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - _3 = reader.readInt32() - var _4: [Int64]? + if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } + var _4: [Api.ReactionCount]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) + } } + var _5: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } + var _6: [Int64]? if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + _6 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, reactionsCount: _3!, recentViewers: _4) + let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, forwardsCount: _3, reactions: _4, reactionsCount: _5, recentViewers: _6) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index eeaff7396f..997719f0cd 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -53,16 +53,19 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe case let .messageActionBotAllowed(flags, domain, app): if let domain = domain { return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain)) - } else if case let .botApp(_, _, _, _, appName, _, _, _, _) = app { + } else { + var appName: String? + if case let .botApp(_, _, _, _, appNameValue, _, _, _, _) = app { + appName = appNameValue + } var type: BotSendMessageAccessGrantedType? - if (flags & (1 << 3)) != 0 { - type = .request - } else if (flags & (1 << 1)) != 0 { + if (flags & (1 << 1)) != 0 { type = .attachMenu } + if (flags & (1 << 3)) != 0 { + type = .request + } return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type)) - } else { - return nil } case .messageActionSecureValuesSentMe: return nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift index 2ec5399c1e..b8c99a0b3f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift @@ -56,7 +56,11 @@ public class AutoclearTimeoutMessageAttribute: MessageAttribute { self.countdownBeginTime = countdownBeginTime if let countdownBeginTime = countdownBeginTime { - self.automaticTimestampBasedAttribute = (1, countdownBeginTime + timeout) + if self.timeout == viewOnceTimeout { + self.automaticTimestampBasedAttribute = (1, countdownBeginTime) + } else { + self.automaticTimestampBasedAttribute = (1, countdownBeginTime + timeout) + } } else { self.automaticTimestampBasedAttribute = nil } @@ -67,7 +71,11 @@ public class AutoclearTimeoutMessageAttribute: MessageAttribute { self.countdownBeginTime = decoder.decodeOptionalInt32ForKey("c") if let countdownBeginTime = self.countdownBeginTime { - self.automaticTimestampBasedAttribute = (1, countdownBeginTime + self.timeout) + if self.timeout == viewOnceTimeout { + self.automaticTimestampBasedAttribute = (1, countdownBeginTime) + } else { + self.automaticTimestampBasedAttribute = (1, countdownBeginTime + self.timeout) + } } else { self.automaticTimestampBasedAttribute = nil } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index aa7e0e99de..dc323b533c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -1,7 +1,7 @@ import Postbox import TelegramApi -public struct MessageReaction: Equatable, PostboxCoding { +public struct MessageReaction: Equatable, PostboxCoding, Codable { public enum Reaction: Hashable, Codable, PostboxCoding { case builtin(String) case custom(Int64) @@ -75,6 +75,24 @@ public struct MessageReaction: Equatable, PostboxCoding { } } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let value = try container.decodeIfPresent(String.self, forKey: "v") { + self.value = .builtin(value) + } else { + self.value = .custom(try container.decode(Int64.self, forKey: "cfid")) + } + self.count = try container.decode(Int32.self, forKey: "c") + if let chosenOrder = try container.decodeIfPresent(Int32.self, forKey: "s") { + self.chosenOrder = Int(chosenOrder) + } else if let isSelected = try container.decodeIfPresent(Int32.self, forKey: "s"), isSelected != 0 { + self.chosenOrder = 0 + } else { + self.chosenOrder = nil + } + } + public func encode(_ encoder: PostboxEncoder) { switch self.value { case let .builtin(value): @@ -89,6 +107,19 @@ public struct MessageReaction: Equatable, PostboxCoding { encoder.encodeNil(forKey: "cord") } } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self.value { + case let .builtin(value): + try container.encode(value, forKey: "v") + case let .custom(fileId): + try container.encode(fileId, forKey: "cfid") + } + try container.encode(self.count, forKey: "c") + try container.encodeIfPresent(self.chosenOrder.flatMap(Int32.init), forKey: "cord") + } } extension MessageReaction.Reaction { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 829908f62b..9e1cf940a1 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -91,7 +91,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool) case customText(text: String, entities: [MessageTextEntity]) case botDomainAccessGranted(domain: String) - case botAppAccessGranted(appName: String, type: BotSendMessageAccessGrantedType?) + case botAppAccessGranted(appName: String?, type: BotSendMessageAccessGrantedType?) case botSentSecureValues(types: [SentSecureValueType]) case peerJoined case phoneNumberRequest @@ -202,7 +202,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .unknown } case 35: - self = .botAppAccessGranted(appName: decoder.decodeStringForKey("app", orElse: ""), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) + self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) default: self = .unknown } @@ -372,7 +372,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper") case let .botAppAccessGranted(appName, type): encoder.encodeInt32(35, forKey: "_rawValue") - encoder.encodeString(appName, forKey: "app") + if let appName = appName { + encoder.encodeString(appName, forKey: "app") + } else { + encoder.encodeNil(forKey: "app") + } if let type = type { encoder.encodeInt32(type.rawValue, forKey: "atp") } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index c2cae9d9bd..1549d52b4a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -358,7 +358,14 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds, hasList: currentViews.hasList), + views: Stories.Item.Views( + seenCount: Int(count), + reactedCount: Int(reactionsCount), + forwardCount: currentViews.forwardCount, + seenPeerIds: currentViews.seenPeerIds, + reactions: currentViews.reactions, + hasList: currentViews.hasList + ), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -387,7 +394,14 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds, hasList: currentViews.hasList), + views: Stories.Item.Views( + seenCount: Int(count), + reactedCount: Int(reactionsCount), + forwardCount: currentViews.forwardCount, + seenPeerIds: currentViews.seenPeerIds, + reactions: currentViews.reactions, + hasList: currentViews.hasList + ), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift index 1916744a13..46be899ce2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift @@ -83,7 +83,7 @@ func _internal_markMessageContentAsConsumedInteractively(postbox: Postbox, messa } else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute { if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 { var timeout = attribute.timeout - if let duration = message.secretMediaDuration { + if let duration = message.secretMediaDuration, timeout != viewOnceTimeout { timeout = max(timeout, Int32(duration)) } updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 0715ce9f37..f8c3e09b9e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -42,19 +42,25 @@ public enum Stories { private enum CodingKeys: String, CodingKey { case seenCount = "seenCount" case reactedCount = "reactedCount" + case forwardCount = "forwardCount" case seenPeerIds = "seenPeerIds" + case reactions = "reactions" case hasList = "hasList" } public var seenCount: Int public var reactedCount: Int + public var forwardCount: Int public var seenPeerIds: [PeerId] + public var reactions: [MessageReaction] public var hasList: Bool - public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId], hasList: Bool) { + public init(seenCount: Int, reactedCount: Int, forwardCount: Int, seenPeerIds: [PeerId], reactions: [MessageReaction], hasList: Bool) { self.seenCount = seenCount self.reactedCount = reactedCount + self.forwardCount = forwardCount self.seenPeerIds = seenPeerIds + self.reactions = reactions self.hasList = hasList } @@ -63,7 +69,9 @@ public enum Stories { self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount)) self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0) + self.forwardCount = Int(try container.decodeIfPresent(Int32.self, forKey: .forwardCount) ?? 0) self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init) + self.reactions = try container.decodeIfPresent([MessageReaction].self, forKey: .reactions) ?? [] self.hasList = try container.decodeIfPresent(Bool.self, forKey: .hasList) ?? true } @@ -72,7 +80,9 @@ public enum Stories { try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount) try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount) + try container.encode(Int32(clamping: self.forwardCount), forKey: .forwardCount) try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds) + try container.encode(self.reactions, forKey: .reactions) try container.encode(self.hasList, forKey: .hasList) } } @@ -1000,6 +1010,15 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId updatedItems.append(updatedItem) } transaction.setStoryItems(peerId: toPeerId, items: items) + + if let peer = transaction.getPeer(toPeerId) as? TelegramChannel, let storiesHidden = peer.storiesHidden { + let subscriptionsKey: PostboxStorySubscriptionsKey = storiesHidden ? .hidden : .filtered + var (state, peerIds) = transaction.getAllStorySubscriptions(key: subscriptionsKey) + if !peerIds.contains(toPeerId) { + peerIds.append(toPeerId) + } + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } } id = idValue @@ -1436,13 +1455,32 @@ extension Api.StoryItem { extension Stories.Item.Views { init(apiViews: Api.StoryViews) { switch apiViews { - case let .storyViews(flags, viewsCount, reactionsCount, recentViewers): + case let .storyViews(flags, viewsCount, forwardsCount, reactions, reactionsCount, recentViewers): + //storyViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector reactions_count:flags.4?int recent_viewers:flags.0?Vector = StoryViews; let hasList = (flags & (1 << 1)) != 0 var seenPeerIds: [PeerId] = [] if let recentViewers = recentViewers { seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } } - self.init(seenCount: Int(viewsCount), reactedCount: Int(reactionsCount), seenPeerIds: seenPeerIds, hasList: hasList) + var mappedReactions: [MessageReaction] = [] + if let reactions = reactions { + for result in reactions { + switch result { + case let .reactionCount(_, chosenOrder, reaction, count): + if let reaction = MessageReaction.Reaction(apiReaction: reaction) { + mappedReactions.append(MessageReaction(value: reaction, count: count, chosenOrder: chosenOrder.flatMap(Int.init))) + } + } + } + } + self.init( + seenCount: Int(viewsCount), + reactedCount: Int(reactionsCount ?? 0), + forwardCount: Int(forwardsCount ?? 0), + seenPeerIds: seenPeerIds, + reactions: mappedReactions, + hasList: hasList + ) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 3713d7cd99..9a62305449 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -14,13 +14,17 @@ public final class EngineStoryItem: Equatable { public final class Views: Equatable { public let seenCount: Int public let reactedCount: Int + public var forwardCount: Int public let seenPeers: [EnginePeer] + public let reactions: [MessageReaction] public let hasList: Bool - public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer], hasList: Bool) { + public init(seenCount: Int, reactedCount: Int, forwardCount: Int, seenPeers: [EnginePeer], reactions: [MessageReaction], hasList: Bool) { self.seenCount = seenCount self.reactedCount = reactedCount + self.forwardCount = forwardCount self.seenPeers = seenPeers + self.reactions = reactions self.hasList = hasList } @@ -31,9 +35,15 @@ public final class EngineStoryItem: Equatable { if lhs.reactedCount != rhs.reactedCount { return false } + if lhs.forwardCount != rhs.forwardCount { + return false + } if lhs.seenPeers != rhs.seenPeers { return false } + if lhs.reactions != rhs.reactions { + return false + } if lhs.hasList != rhs.hasList { return false } @@ -159,7 +169,9 @@ extension EngineStoryItem { return Stories.Item.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeerIds: views.seenPeers.map(\.id), + reactions: views.reactions, hasList: views.hasList ) }, @@ -533,9 +545,11 @@ public final class PeerStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -661,9 +675,11 @@ public final class PeerStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -813,9 +829,11 @@ public final class PeerStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -856,9 +874,11 @@ public final class PeerStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -901,9 +921,11 @@ public final class PeerStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -942,9 +964,11 @@ public final class PeerStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -1107,9 +1131,11 @@ public final class PeerExpiringStoryListContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 957a100c32..edc2ac5dd9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -953,6 +953,10 @@ public extension TelegramEngine { return lhs.peer.id < rhs.peer.id }) + if !isHidden { + assert(true) + } + return EngineStorySubscriptions(accountItem: accountItem, items: items, hasMoreToken: hasMoreToken) } } diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index e53908e1c5..8708be13c5 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -126,7 +126,7 @@ func _internal_updatePeerIsContact(transaction: Transaction, user: TelegramUser, } } -private func _internal_updateChannelMembership(transaction: Transaction, channel: TelegramChannel, isMember: Bool) { +private func _internal_updateChannelMembership(transaction: Transaction, channel: TelegramChannel, isMember: Bool, justJoined: Bool) { if isMember, let storiesHidden = channel.storiesHidden { if storiesHidden { if transaction.storySubscriptionsContains(key: .filtered, peerId: channel.id) { @@ -155,6 +155,10 @@ private func _internal_updateChannelMembership(transaction: Transaction, channel } } } + + if justJoined { + _internal_addSynchronizePeerStoriesOperation(peerId: channel.id, transaction: transaction) + } } else { if transaction.storySubscriptionsContains(key: .filtered, peerId: channel.id) { var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) @@ -191,7 +195,7 @@ public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: ( if let updated = updated as? TelegramChannel { let isMember = updated.participationStatus == .member if isMember != wasMember || updated.storiesHidden != wasHidden { - _internal_updateChannelMembership(transaction: transaction, channel: updated, isMember: isMember) + _internal_updateChannelMembership(transaction: transaction, channel: updated, isMember: isMember, justJoined: previous == nil || wasHidden == nil) } } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 1159187c22..0998610d15 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -629,8 +629,16 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()) case let .botDomainAccessGranted(domain): attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor) - case let .botAppAccessGranted(appName, _): - attributedString = NSAttributedString(string: strings.AuthSessions_MessageApp(appName).string, font: titleFont, textColor: primaryTextColor) + case let .botAppAccessGranted(appName, type): + let text: String + if type == .attachMenu { + text = strings.Notification_BotWriteAllowedMenu + } else if type == .request { + text = strings.Notification_BotWriteAllowedRequest + } else { + text = strings.AuthSessions_MessageApp(appName ?? "").string + } + attributedString = NSAttributedString(string: text, font: titleFont, textColor: primaryTextColor) case let .botSentSecureValues(types): var typesString = "" var hasIdentity = false diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 6ba26378ce..12ecfd9e54 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1466,6 +1466,7 @@ public class CameraScreen: ViewController { }) } + fileprivate var captureStartTimestamp: Double? private func setupCamera() { guard self.camera == nil else { return @@ -1575,6 +1576,7 @@ public class CameraScreen: ViewController { camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) camera.startCapture() + self.captureStartTimestamp = CACurrentMediaTime() self.camera = camera @@ -2515,8 +2517,19 @@ public class CameraScreen: ViewController { guard let self, !self.didStopCameraCapture else { return } - self.didStopCameraCapture = true - self.node.pauseCameraCapture() + let currentTimestamp = CACurrentMediaTime() + if let startTimestamp = self.node.captureStartTimestamp { + let difference = currentTimestamp - startTimestamp + if difference < 2.0 { + Queue.mainQueue().after(2.0 - difference) { + self.didStopCameraCapture = true + self.node.pauseCameraCapture() + } + } else { + self.didStopCameraCapture = true + self.node.pauseCameraCapture() + } + } } let resumeCameraCapture = { [weak self] in diff --git a/submodules/TelegramUI/Components/ContextReferenceButtonComponent/BUILD b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/BUILD new file mode 100644 index 0000000000..b41112ece6 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ContextReferenceButtonComponent", + module_name = "ContextReferenceButtonComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/ContextUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift new file mode 100644 index 0000000000..6181c32abb --- /dev/null +++ b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift @@ -0,0 +1,123 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class ContextReferenceButtonComponent: Component { + let content: AnyComponent + let tag: AnyObject? + let minSize: CGSize? + let action: (UIView, ContextGesture?) -> Void + + public init( + content: AnyComponent, + tag: AnyObject? = nil, + minSize: CGSize?, + action: @escaping (UIView, ContextGesture?) -> Void + ) { + self.content = content + self.tag = tag + self.minSize = minSize + self.action = action + } + + public static func ==(lhs: ContextReferenceButtonComponent, rhs: ContextReferenceButtonComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.tag !== rhs.tag { + return false + } + if lhs.minSize != rhs.minSize { + return false + } + return true + } + + public final class View: UIView, ComponentTaggedView { + let buttonView: HighlightableButtonNode + let sourceView: ContextControllerSourceNode + let contextContentView: ContextReferenceContentNode + + private let componentView: ComponentView + + private var component: ContextReferenceButtonComponent? + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + public init() { + self.componentView = ComponentView() + self.buttonView = HighlightableButtonNode() + self.sourceView = ContextControllerSourceNode() + self.contextContentView = ContextReferenceContentNode() + + super.init(frame: CGRect()) + + self.addSubview(self.buttonView.view) + self.buttonView.addSubnode(self.sourceView) + self.sourceView.addSubnode(self.contextContentView) + + self.sourceView.activated = { [weak self] gesture, _ in + if let self, let component = self.component { + component.action(self, gesture) + } + } + self.buttonView.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + @objc private func pressed() { + self.component?.action(self, nil) + } + + public func update(component: ContextReferenceButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + let componentSize = self.componentView.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: availableSize + ) + + var size = componentSize + if let minSize = component.minSize { + size.width = max(size.width, minSize.width) + size.height = max(size.height, minSize.height) + } + + if let componentView = self.componentView.view { + componentView.isUserInteractionEnabled = false + if componentView.superview == nil { + self.contextContentView.view.addSubview(componentView) + } + transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: floor((size.width - componentSize.width) / 2.0), y: floor((size.height - componentSize.height) / 2.0)), size: componentSize)) + } + + transition.setFrame(view: self.buttonView.view, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.sourceView.view, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.contextContentView.view, frame: CGRect(origin: .zero, size: size)) + + return size + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index ebc43e38ec..bacea16101 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -69,9 +69,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } } + private var scheduledMessageInput: MessageInputPanelComponent.SendMessageInput? public func setCaption(_ caption: NSAttributedString?) { + let sendMessageInput = MessageInputPanelComponent.SendMessageInput.text(caption ?? NSAttributedString()) if let view = self.inputPanel.view as? MessageInputPanelComponent.View { - view.setSendMessageInput(value: .text(caption ?? NSAttributedString()), updateState: true) + view.setSendMessageInput(value: sendMessageInput, updateState: true) + } else { + self.scheduledMessageInput = sendMessageInput } } @@ -81,6 +85,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } public func setTimeout(_ timeout: Int32) { + self.dismissTimeoutTooltip() var timeout: Int32? = timeout if timeout == 0 { timeout = nil @@ -142,6 +147,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { maxInputPanelHeight = 60.0 } + var resetInputContents: MessageInputPanelComponent.SendMessageInput? + if let scheduledMessageInput = self.scheduledMessageInput { + resetInputContents = scheduledMessageInput + self.scheduledMessageInput = nil + } + self.inputPanel.parentState = self.state let inputPanelSize = self.inputPanel.update( transition: Transition(transition), @@ -156,7 +167,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { maxLength: 1024, queryTypes: [.mention], alwaysDarkWhenHasText: false, - resetInputContents: nil, + resetInputContents: resetInputContents, nextInputMode: { _ in return .emoji }, @@ -180,9 +191,9 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { likeAction: nil, likeOptionsAction: nil, inputModeAction: nil, - timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView in + timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView, gesture in if let self { - self.presentTimeoutSetup(sourceView: sourceView) + self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) } } : nil, forwardAction: nil, @@ -234,7 +245,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { return inputPanelSize.height - 8.0 } - private func presentTimeoutSetup(sourceView: UIView) { + private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) { self.hapticFeedback.impact(.light) var items: [ContextMenuItem] = [] @@ -250,12 +261,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { let currentValue = self.currentTimeout let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) - let title = "Choose how long the media will be kept after opening." + let title = presentationData.strings.MediaPicker_Timer_Description let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) - items.append(.action(ContextMenuActionItem(text: "View Once", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_ViewOnce, icon: { theme in return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) @@ -263,31 +274,19 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { updateTimeout(viewOnceTimeout) }))) - items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { theme in - return currentValue == 3 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) - - updateTimeout(3) - }))) + let values: [Int32] = [3, 10, 30] - items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { theme in - return currentValue == 10 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) + for value in values { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_Seconds(value), icon: { theme in + return currentValue == value ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil + }, action: { _, a in + a(.default) + + updateTimeout(value) + }))) + } - updateTimeout(10) - }))) - - items.append(.action(ContextMenuActionItem(text: "30 Seconds", icon: { theme in - return currentValue == 30 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) - - updateTimeout(30) - }))) - - items.append(.action(ContextMenuActionItem(text: "Do Not Delete", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) @@ -295,34 +294,41 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { updateTimeout(nil) }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController) } private weak var tooltipController: TooltipScreen? - private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) { - guard let superview = self.view.superview?.superview else { - return - } + + private func dismissTimeoutTooltip() { if let tooltipController = self.tooltipController { self.tooltipController = nil tooltipController.dismiss() } + } + + private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) { + guard let superview = self.view.superview?.superview else { + return + } + self.dismissTimeoutTooltip() let parentFrame = superview.convert(superview.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize()) + let isVideo = !"".isEmpty + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let text: String let iconName: String if timeout == viewOnceTimeout { - text = "Photo set to view once." + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_ViewOnceTooltip : presentationData.strings.MediaPicker_Timer_Photo_ViewOnceTooltip iconName = "anim_autoremove_on" } else if let timeout { - text = "Photo will be deleted in \(timeout) seconds after opening." + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_TimerTooltip("\(timeout)").string : presentationData.strings.MediaPicker_Timer_Photo_TimerTooltip("\(timeout)").string iconName = "anim_autoremove_on" } else { - text = "Photo will be kept in chat." + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip iconName = "anim_autoremove_off" } @@ -330,7 +336,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), - balancedTextLayout: true, + balancedTextLayout: false, style: .customBlur(UIColor(rgb: 0x18181a), 0.0), arrowStyle: .small, icon: .animation(name: iconName, delay: 0.1, tintColor: nil), diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 5b39d0811b..63c713f057 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -389,7 +389,7 @@ public final class MediaEditorVideoExport { try? videoTrack.insertTimeRange(timeRange, of: videoAssetTrack, at: .zero) - if let audioAssetTrack = asset.tracks(withMediaType: .audio).first, let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) { + if let audioAssetTrack = asset.tracks(withMediaType: .audio).first, let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid), !self.configuration.values.videoIsMuted { try? audioTrack.insertTimeRange(timeRange, of: audioAssetTrack, at: .zero) } @@ -488,7 +488,7 @@ public final class MediaEditorVideoExport { } let audioTracks = inputAsset.tracks(withMediaType: .audio) - if audioTracks.count > 0, !self.configuration.values.videoIsMuted { + if audioTracks.count > 0, !self.configuration.values.videoIsMuted || self.configuration.values.audioTrack != nil { let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil) audioOutput.alwaysCopiesSampleData = false if reader.canAdd(audioOutput) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 8c88fd1dfa..9dbc5dc3bf 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1117,7 +1117,7 @@ final class MediaEditorScreenComponent: Component { } } }, - timeoutAction: isEditingStory ? nil : { [weak self] view in + timeoutAction: isEditingStory ? nil : { [weak self] view, gesture in guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { return } @@ -1130,7 +1130,7 @@ final class MediaEditorScreenComponent: Component { } else { hasPremium = false } - controller?.presentTimeoutSetup(sourceView: view, hasPremium: hasPremium) + controller?.presentTimeoutSetup(sourceView: view, gesture: gesture, hasPremium: hasPremium) }) }, forwardAction: nil, @@ -2778,7 +2778,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) - let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: isMuted ? self.presentationData.strings.Story_Editor_TooltipMuted : self.presentationData.strings.Story_Editor_TooltipUnmuted), location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + let text: String + if let _ = self.mediaEditor?.values.audioTrack { + if isMuted { + text = self.presentationData.strings.Story_Editor_TooltipMutedWithAudio + } else { + text = self.presentationData.strings.Story_Editor_TooltipUnmutedWithAudio + } + } else { + if isMuted { + text = self.presentationData.strings.Story_Editor_TooltipMuted + } else { + text = self.presentationData.strings.Story_Editor_TooltipUnmuted + } + } + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in return .ignore }) self.muteTooltip = tooltipController @@ -2957,7 +2972,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate location = draft.location } } - let locationController = storyLocationPickerController(context: self.context, location: location, completion: { [weak self] location, queryId, resultId, address, countryCode in + let locationController = storyLocationPickerController( + context: self.context, + location: location, + dismissed: { [weak self] in + if let self { + self.mediaEditor?.play() + } + }, + completion: { [weak self] location, queryId, resultId, address, countryCode in if let self { let emojiFile: Signal if let countryCode { @@ -3035,7 +3058,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } func presentAudioPicker() { - self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3"], forceDarkTheme: true, completion: { [weak self] urls in + self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3"], forceDarkTheme: true, dismissed: { [weak self] in + if let self { + Queue.mainQueue().after(0.1) { + self.mediaEditor?.play() + } + } + }, completion: { [weak self] urls in guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else { return } @@ -3060,10 +3089,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.requestUpdate(transition: .easeInOut(duration: 0.2)) - - Queue.mainQueue().after(0.1) { - self.mediaEditor?.play() - } }), in: .window(.root)) } @@ -3081,7 +3106,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate action: { [weak self] f in f.dismissWithResult(.default) if let self { - self.mediaEditor?.setAudioTrack(nil) + if let mediaEditor = self.mediaEditor { + mediaEditor.setAudioTrack(nil) + + if !mediaEditor.sourceIsVideo && !mediaEditor.isPlaying { + mediaEditor.play() + } + } self.requestUpdate(transition: .easeInOut(duration: 0.25)) } } @@ -3872,7 +3903,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) } - func presentTimeoutSetup(sourceView: UIView, hasPremium: Bool) { + func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?, hasPremium: Bool) { self.hapticFeedback.impact(.light) var items: [ContextMenuItem] = [] @@ -3893,7 +3924,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let title = presentationData.strings.Story_Editor_ExpirationText let currentValue = self.state.privacy.timeout - let currentArchived = self.state.privacy.pin let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) @@ -3929,7 +3959,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(24), icon: { theme in - return currentValue == 86400 && !currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil + return currentValue == 86400 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil }, action: { _, a in a(.default) @@ -3951,7 +3981,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }))) - let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index 76e70fbb35..225d0f982e 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -37,6 +37,8 @@ swift_library( "//submodules/AnimatedCountLabelNode", "//submodules/TelegramUI/Components/MessageInputActionButtonComponent", "//submodules/SearchPeerMembers", + "//submodules/ContextUI", + "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 911e501c3a..898f0aee37 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -18,6 +18,9 @@ import AudioToolbox import AnimatedTextComponent import AnimatedCountLabelNode import MessageInputActionButtonComponent +import ContextReferenceButtonComponent + +private let timeoutButtonTag = GenericComponentViewTag() public final class MessageInputPanelComponent: Component { public struct ContextQueryTypes: OptionSet { @@ -120,7 +123,7 @@ public final class MessageInputPanelComponent: Component { public let likeAction: (() -> Void)? public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)? public let inputModeAction: (() -> Void)? - public let timeoutAction: ((UIView) -> Void)? + public let timeoutAction: ((UIView, ContextGesture?) -> Void)? public let forwardAction: (() -> Void)? public let moreAction: ((UIView, ContextGesture?) -> Void)? public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)? @@ -172,7 +175,7 @@ public final class MessageInputPanelComponent: Component { likeAction: (() -> Void)?, likeOptionsAction: ((UIView, ContextGesture?) -> Void)?, inputModeAction: (() -> Void)?, - timeoutAction: ((UIView) -> Void)?, + timeoutAction: ((UIView, ContextGesture?) -> Void)?, forwardAction: (() -> Void)?, moreAction: ((UIView, ContextGesture?) -> Void)?, presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?, @@ -1456,49 +1459,23 @@ public final class MessageInputPanelComponent: Component { let accentColor = component.theme.chat.inputPanel.panelControlAccentColor if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue { - func generateIcon(value: String, selected: Bool) -> UIImage? { - let image = UIImage(bundleImageName: "Media Editor/Timeout")! - let valueString = NSAttributedString(string: value, font: Font.with(size: value.count == 1 ? 12.0 : 10.0, design: .round, weight: .semibold), textColor: .white, paragraphAlignment: .center) - - return generateImage(image.size, contextGenerator: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - if selected { - context.setFillColor(accentColor.cgColor) - context.fillEllipse(in: CGRect(origin: .zero, size: size)) - } else { - if let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - } - } - - var offset: CGPoint = CGPoint(x: 0.0, y: -3.0 - UIScreenPixel) - if value == "∞" { - offset.x += UIScreenPixel - offset.y += 1.0 - UIScreenPixel - } - - let valuePath = CGMutablePath() - valuePath.addRect(bounds.offsetBy(dx: offset.x, dy: offset.y)) - let valueFramesetter = CTFramesetterCreateWithAttributedString(valueString as CFAttributedString) - let valyeFrame = CTFramesetterCreateFrame(valueFramesetter, CFRangeMake(0, valueString.length), valuePath, nil) - CTFrameDraw(valyeFrame, context) - }) - } - - let icon = generateIcon(value: timeoutValue, selected: component.timeoutSelected) let timeoutButtonSize = self.timeoutButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Image(image: icon, size: CGSize(width: 20.0, height: 20.0))), - action: { [weak self] in - guard let self, let timeoutButtonView = self.timeoutButton.view else { - return - } - timeoutAction(timeoutButtonView) + component: AnyComponent(ContextReferenceButtonComponent( + content: AnyComponent( + TimeoutContentComponent( + color: .white, + accentColor: accentColor, + isSelected: component.timeoutSelected, + value: timeoutValue + ) + ), + tag: timeoutButtonTag, + minSize: CGSize(width: 32.0, height: 32.0), + action: { view, gesture in + timeoutAction(view, gesture) } - ).minSize(CGSize(width: 32.0, height: 32.0))), + )), environment: {}, containerSize: CGSize(width: 32.0, height: 32.0) ) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift new file mode 100644 index 0000000000..c8c854e20e --- /dev/null +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift @@ -0,0 +1,137 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class TimeoutContentComponent: Component { + public let color: UIColor + public let accentColor: UIColor + public let isSelected: Bool + public let value: String + + public init( + color: UIColor, + accentColor: UIColor, + isSelected: Bool, + value: String + ) { + self.color = color + self.accentColor = accentColor + self.isSelected = isSelected + self.value = value + } + + public static func ==(lhs: TimeoutContentComponent, rhs: TimeoutContentComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private var component: TimeoutContentComponent? + private weak var state: EmptyComponentState? + + private let background: UIImageView + private let foreground: UIImageView + private let text = ComponentView() + + override init(frame: CGRect) { + self.background = UIImageView(image: UIImage(bundleImageName: "Media Editor/Timeout")) + self.foreground = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.background) + self.addSubview(self.foreground) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TimeoutContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + let size = CGSize(width: 20.0, height: 20.0) + if previousComponent?.accentColor != component.accentColor { + self.foreground.image = generateFilledCircleImage(diameter: size.width, color: component.accentColor) + } + + var updated = false + if let previousComponent { + if previousComponent.isSelected != component.isSelected { + updated = true + } + if previousComponent.value != component.value { + if let textView = self.text.view, let snapshotView = textView.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = textView.frame + self.addSubview(snapshotView) + snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -3.0), 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() + }) + + textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + textView.layer.animatePosition(from: CGPoint(x: 0.0, y: 3.0), to: .zero, duration: 0.2, additive: true) + } + } + } + + let fontSize: CGFloat + let textOffset: CGFloat + if component.value.count == 1 { + fontSize = 12.0 + textOffset = UIScreenPixel + } else { + fontSize = 10.0 + textOffset = -UIScreenPixel + } + + let font = Font.with(size: fontSize, design: .round, weight: .semibold) + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(Text(text: component.value, font: font, color: .white)), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + textOffset), size: textSize) + transition.setPosition(view: textView, position: textFrame.center) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + self.background.frame = CGRect(origin: .zero, size: size) + + self.foreground.bounds = CGRect(origin: .zero, size: size) + self.foreground.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + let foregroundTransition: Transition = updated ? .easeInOut(duration: 0.2) : transition + foregroundTransition.setScale(view: self.foreground, scale: component.isSelected ? 1.0 : 0.001) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 36c88fb6a3..cbd641d986 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -481,21 +481,29 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } private final class ItemTransitionView: UIView { - private weak var itemLayer: ItemLayer? + private weak var itemLayer: CALayer? private var copyDurationLayer: SimpleLayer? private var durationLayerBottomLeftPosition: CGPoint? - init(itemLayer: ItemLayer?) { + init(itemLayer: CALayer?) { self.itemLayer = itemLayer super.init(frame: CGRect()) if let itemLayer { - self.layer.contents = itemLayer.contents self.layer.contentsRect = itemLayer.contentsRect - if let durationLayer = itemLayer.durationLayer { + var durationLayer: CALayer? + if let itemLayer = itemLayer as? CaptureProtectedItemLayer { + durationLayer = itemLayer.durationLayer + self.layer.contents = itemLayer.getContents() + } else if let itemLayer = itemLayer as? ItemLayer { + durationLayer = itemLayer.durationLayer + self.layer.contents = itemLayer.contents + } + + if let durationLayer { let copyDurationLayer = SimpleLayer() copyDurationLayer.contents = durationLayer.contents copyDurationLayer.contentsRect = durationLayer.contentsRect @@ -1074,7 +1082,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr destinationView: self.view, transitionView: StoryContainerScreen.TransitionView( makeView: { [weak foundItemLayer] in - return ItemTransitionView(itemLayer: foundItemLayer as? ItemLayer) + return ItemTransitionView(itemLayer: foundItemLayer) }, updateView: { view, state, transition in (view as? ItemTransitionView)?.update(state: state, transition: transition) @@ -1104,10 +1112,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return } if let itemId { + let anyAmount = self.itemInteraction.hiddenMedia.isEmpty self.itemInteraction.hiddenMedia = Set([itemId.id]) + if let items = self.items, let item = items.items.first(where: { $0.id == AnyHashable(itemId.id) }) { + self.itemGrid.ensureItemVisible(index: item.index, anyAmount: anyAmount) + } } else { self.itemInteraction.hiddenMedia = Set() } + self.updateHiddenItems() }) diff --git a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD new file mode 100644 index 0000000000..51b983ac39 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NewSessionInfoScreen", + module_name = "NewSessionInfoScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/Components/SheetComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift new file mode 100644 index 0000000000..49f0ec5d2c --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoContentComponent.swift @@ -0,0 +1,332 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import AppBundle +import BundleIconComponent +import Markdown +import TelegramCore + +public final class NewSessionInfoContentComponent: Component { + public let theme: PresentationTheme + public let strings: PresentationStrings + public let settings: GlobalPrivacySettings + public let openSettings: () -> Void + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + settings: GlobalPrivacySettings, + openSettings: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.settings = settings + self.openSettings = openSettings + } + + public static func ==(lhs: NewSessionInfoContentComponent, rhs: NewSessionInfoContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.settings != rhs.settings { + return false + } + return true + } + + private final class Item { + let icon = ComponentView() + let title = ComponentView() + let text = ComponentView() + + init() { + } + } + + public final class View: UIView { + private let scrollView: UIScrollView + private let iconBackground: UIImageView + private let iconForeground: UIImageView + + private let title = ComponentView() + private let mainText = ComponentView() + + private var chevronImage: UIImage? + + private var items: [Item] = [] + + private var component: NewSessionInfoContentComponent? + + public override init(frame: CGRect) { + self.scrollView = UIScrollView() + + self.iconBackground = UIImageView() + self.iconForeground = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.scrollView) + + self.scrollView.delaysContentTouches = false + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = false + + self.scrollView.addSubview(self.iconBackground) + self.scrollView.addSubview(self.iconForeground) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = super.hitTest(point, with: event) { + return result + } else { + return nil + } + } + + func update(component: NewSessionInfoContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 16.0 + let sideIconInset: CGFloat = 40.0 + + var contentHeight: CGFloat = 0.0 + + let iconSize: CGFloat = 90.0 + if self.iconBackground.image == nil { + let backgroundColors = component.theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors + let colors: NSArray = [backgroundColors.1.cgColor, backgroundColors.0.cgColor] + self.iconBackground.image = generateGradientFilledCircleImage(diameter: iconSize, colors: colors) + } + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize) * 0.5), y: contentHeight), size: CGSize(width: iconSize, height: iconSize)) + transition.setFrame(view: self.iconBackground, frame: iconBackgroundFrame) + + if self.iconForeground.image == nil { + self.iconForeground.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/ArchiveIconLarge"), color: .white) + } + if let image = self.iconForeground.image { + transition.setFrame(view: self.iconForeground, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) + } + + contentHeight += iconSize + contentHeight += 15.0 + + let titleString = NSMutableAttributedString() + titleString.append(NSAttributedString(string: component.strings.NewSessionInfo_Title, font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) + let imageAttachment = NSTextAttachment() + imageAttachment.image = self.iconBackground.image + titleString.append(NSAttributedString(attachment: imageAttachment)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleString), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 16.0 + + let text: String + if component.settings.keepArchivedUnmuted { + text = component.strings.NewSessionInfo_TextKeepArchivedUnmuted + } else { + text = component.strings.NewSessionInfo_TextKeepArchivedDefault + } + + let mainText = NSMutableAttributedString() + mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + bold: MarkdownAttributeSet( + font: Font.semibold(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + link: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemAccentColor, + additionalAttributes: [:] + ), + linkAttribute: { attributes in + return ("URL", "") + } + ))) + if self.chevronImage == nil { + self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight") + } + if let range = mainText.string.range(of: ">"), let chevronImage = self.chevronImage { + mainText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mainText.string)) + } + + let mainTextSize = self.mainText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(mainText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + component.openSettings() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let mainTextView = self.mainText.view { + if mainTextView.superview == nil { + self.scrollView.addSubview(mainTextView) + } + transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + } + contentHeight += mainTextSize.height + + contentHeight += 24.0 + + struct ItemDesc { + var icon: String + var title: String + var text: String + } + let itemDescs: [ItemDesc] = [ + ItemDesc( + icon: "Chat List/Archive/IconArchived", + title: component.strings.NewSessionInfo_ChatsTitle, + text: component.strings.NewSessionInfo_ChatsText + ), + ItemDesc( + icon: "Chat List/Archive/IconHide", + title: component.strings.NewSessionInfo_HideTitle, + text: component.strings.NewSessionInfo_HideText + ), + ItemDesc( + icon: "Chat List/Archive/IconStories", + title: component.strings.NewSessionInfo_StoriesTitle, + text: component.strings.NewSessionInfo_StoriesText + ) + ] + for i in 0 ..< itemDescs.count { + if i != 0 { + contentHeight += 24.0 + } + + let item: Item + if self.items.count > i { + item = self.items[i] + } else { + item = Item() + self.items.append(item) + } + + let itemDesc = itemDescs[i] + + let iconSize = item.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: itemDesc.icon, + tintColor: component.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let titleSize = item.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + let textSize = item.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + + if let iconView = item.icon.view { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 4.0), size: iconSize)) + } + + if let titleView = item.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 2.0 + + if let textView = item.text.view { + if textView.superview == nil { + self.scrollView.addSubview(textView) + } + transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: textSize)) + } + contentHeight += textSize.height + } + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + let size = CGSize(width: availableSize.width, height: min(availableSize.height, contentSize.height)) + if self.scrollView.bounds.size != size || self.scrollView.contentSize != contentSize { + self.scrollView.frame = CGRect(origin: CGPoint(), size: size) + self.scrollView.contentSize = contentSize + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift new file mode 100644 index 0000000000..049875fdbc --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift @@ -0,0 +1,287 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import SheetComponent +import ButtonComponent +import TelegramCore + +private final class NewSessionInfoSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let settings: GlobalPrivacySettings + let openSettings: () -> Void + let dismiss: () -> Void + + init( + settings: GlobalPrivacySettings, + openSettings: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.settings = settings + self.openSettings = openSettings + self.dismiss = dismiss + } + + static func ==(lhs: NewSessionInfoSheetContentComponent, rhs: NewSessionInfoSheetContentComponent) -> Bool { + if lhs.settings != rhs.settings { + return false + } + return true + } + + final class View: UIView { + private let content = ComponentView() + private let button = ComponentView() + + private var component: NewSessionInfoSheetContentComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: NewSessionInfoSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[EnvironmentType.self].value + + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 30.0 + + let contentSize = self.content.update( + transition: transition, + component: AnyComponent(NewSessionInfoContentComponent( + theme: environment.theme, + strings: environment.strings, + settings: component.settings, + openSettings: component.openSettings + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: contentSize)) + } + contentHeight += contentSize.height + contentHeight += 30.0 + + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: environment.strings.NewSessionInfo_CloseAction, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + + if environment.safeInsets.bottom.isZero { + contentHeight += 16.0 + } else { + contentHeight += environment.safeInsets.bottom + 14.0 + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class NewSessionInfoScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let settings: GlobalPrivacySettings + let buttonAction: (() -> Void)? + + init( + context: AccountContext, + settings: GlobalPrivacySettings, + buttonAction: (() -> Void)? + ) { + self.context = context + self.settings = settings + self.buttonAction = buttonAction + } + + static func ==(lhs: NewSessionInfoScreenComponent, rhs: NewSessionInfoScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.settings != rhs.settings { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: NewSessionInfoScreenComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: NewSessionInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(NewSessionInfoSheetContentComponent( + settings: component.settings, + openSettings: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + let context = component.context + self.sheetAnimateOut.invoke(Action { [weak context, weak controller] _ in + if let controller, let context { + if let navigationController = controller.navigationController as? NavigationController { + navigationController.pushViewController(context.sharedContext.makeArchiveSettingsController(context: context)) + } + controller.dismiss(completion: nil) + } + }) + }, + dismiss: { [weak self] in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + + guard let self else { + return + } + self.component?.buttonAction?() + }) + } + )), + backgroundColor: .color(environment.theme.list.plainBackgroundColor), + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class NewSessionInfoScreen: ViewControllerComponentContainer { + public init(context: AccountContext, settings: GlobalPrivacySettings, buttonAction: (() -> Void)? = nil) { + super.init(context: context, component: NewSessionInfoScreenComponent( + context: context, + settings: settings, + buttonAction: buttonAction + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 56dbc068be..dc2b29f7a4 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -345,6 +345,8 @@ final class ShareWithPeersScreenComponent: Component { private var searchStateContext: ShareWithPeersScreen.StateContext? private var searchStateDisposable: Disposable? + private let hapticFeedback = HapticFeedback() + private var effectiveStateValue: ShareWithPeersScreen.State? { return self.searchStateContext?.stateValue ?? self.defaultStateValue } @@ -501,6 +503,8 @@ final class ShareWithPeersScreenComponent: Component { let translation = recognizer.translation(in: self) self.dismissPanState = DismissPanState(translation: translation.y) self.state?.updated(transition: .immediate) + + self.updateModalOverlayTransition(transition: .immediate) case .cancelled, .ended: if self.dismissPanState != nil { let translation = recognizer.translation(in: self) @@ -510,8 +514,13 @@ final class ShareWithPeersScreenComponent: Component { if translation.y > 100.0 || velocity.y > 10.0 { controller.requestDismiss() + Queue.mainQueue().justDispatch { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring)) + } } else { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) + self.state?.updated(transition: transition) + self.updateModalOverlayTransition(transition: transition) } } default: @@ -549,7 +558,11 @@ final class ShareWithPeersScreenComponent: Component { case .pin: if self.selectedOptions.contains(.pin) { animationName = "anim_profileadd" - text = presentationData.strings.Story_Privacy_TooltipStoryArchived + if let peerId = self.sendAsPeerId, peerId.namespace != Namespaces.Peer.CloudUser { + text = presentationData.strings.Story_Privacy_TooltipStoryArchivedChannel + } else { + text = presentationData.strings.Story_Privacy_TooltipStoryArchived + } } else { animationName = "anim_autoremove_on" text = presentationData.strings.Story_Privacy_TooltipStoryExpires @@ -776,7 +789,7 @@ final class ShareWithPeersScreenComponent: Component { guard let self else { return } - let controller = ShareWithPeersScreen( + let peersController = ShareWithPeersScreen( context: component.context, initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), stateContext: stateContext, @@ -791,10 +804,40 @@ final class ShareWithPeersScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } ) - self.environment?.controller()?.push(controller) + if let controller = self.environment?.controller() as? ShareWithPeersScreen { + controller.dismissAllTooltips() + controller.push(peersController) + } }) } + private func updateModalOverlayTransition(transition: Transition) { + guard let _ = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + return + } + + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + if let dismissPanState = self.dismissPanState { + topOffset += dismissPanState.translation + } + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + if let controller = environment.controller() { + Queue.mainQueue().justDispatch { + var transition = transition + if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate { + transition = .spring(duration: 0.4) + } + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + } + } + } + private func updateScrolling(transition: Transition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return @@ -813,21 +856,7 @@ final class ShareWithPeersScreenComponent: Component { var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance bottomAlpha = max(0.0, min(1.0, bottomAlpha)) - let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - if let controller = environment.controller() { - Queue.mainQueue().justDispatch { - var transition = transition - if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate { - transition = .spring(duration: 0.4) - } - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) - } - } + self.updateModalOverlayTransition(transition: transition) var visibleBounds = self.scrollView.bounds visibleBounds.origin.y -= itemLayout.topInset @@ -996,9 +1025,9 @@ final class ShareWithPeersScreenComponent: Component { let subtitle: String? if case .user = peer { - subtitle = "personal account" + subtitle = environment.strings.VoiceChat_PersonalAccount } else { - subtitle = "channel" + subtitle = environment.strings.Channel_Status } var isStories = false @@ -1037,6 +1066,7 @@ final class ShareWithPeersScreenComponent: Component { if isStories { let _ = self.presentSendAsPeer() } else { + self.hapticFeedback.impact(.light) self.environment?.controller()?.dismiss() self.component?.peerCompletion(peer.id) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 016fb713b8..33f4f50b75 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -169,9 +169,11 @@ public final class StoryContentContextImpl: StoryContentContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, @@ -1076,9 +1078,11 @@ public final class SingleStoryContentContextImpl: StoryContentContext { return EngineStoryItem.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, + forwardCount: views.forwardCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) }, + reactions: views.reactions, hasList: views.hasList ) }, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 90f30d29df..d05405eaf5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -338,6 +338,7 @@ private final class StoryContainerScreenComponent: Component { private let backgroundEffectView: BlurredBackgroundView private let focusedItem = ValuePromise(nil, ignoreRepeated: true) + private var stateValue: StoryContentContextState? private var contentUpdatedDisposable: Disposable? private var stealthModeActiveUntilTimestamp: Int32? @@ -412,7 +413,7 @@ private final class StoryContainerScreenComponent: Component { self.layer.addSublayer(self.backgroundLayer) let horizontalPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in - guard let self, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + guard let self, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return [] } if let environment = self.environment, case .regular = environment.metrics.widthClass { @@ -430,7 +431,7 @@ private final class StoryContainerScreenComponent: Component { //TODO:move dismiss pan /*let verticalPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.dismissPanGesture(_:)), allowedDirections: { [weak self] point in - guard let self, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + guard let self, let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return [] } if let environment = self.environment, case .regular = environment.metrics.widthClass { @@ -460,7 +461,7 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return false } - guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return false } if !itemSetComponentView.allowsExternalGestures(point: touch.location(in: itemSetComponentView)) { @@ -480,7 +481,7 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return false } - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { let itemLocation = self.convert(pinchLocation, to: itemSetComponentView) if itemSetComponentView.allowsExternalGestures(point: itemLocation) { @@ -498,7 +499,7 @@ private final class StoryContainerScreenComponent: Component { return } var pinchLocation = pinchLocation - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { pinchLocation = self.convert(pinchLocation, to: itemSetComponentView) } @@ -634,7 +635,7 @@ private final class StoryContainerScreenComponent: Component { } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return false } @@ -678,7 +679,7 @@ private final class StoryContainerScreenComponent: Component { private func updateHorizontalPan(translation: CGPoint) { var translation = translation - if var itemSetPanState = self.itemSetPanState, self.bounds.width > 0.0, let component = self.component, let stateValue = component.content.stateValue, let _ = stateValue.slice { + if var itemSetPanState = self.itemSetPanState, self.bounds.width > 0.0, let stateValue = self.stateValue, let _ = stateValue.slice { func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { let bandedOffset = offset - bandingStart let range: CGFloat = 600.0 @@ -706,7 +707,7 @@ private final class StoryContainerScreenComponent: Component { if var itemSetPanState = self.itemSetPanState { var shouldDismiss = false - if let component = self.component, let stateValue = component.content.stateValue, let _ = stateValue.slice { + if let component = self.component, let stateValue = self.stateValue, let _ = stateValue.slice { var direction: StoryContentContextNavigation.PeerDirection? var mayDismiss = false if itemSetPanState.fraction <= -0.3 { @@ -787,7 +788,7 @@ private final class StoryContainerScreenComponent: Component { case .began: self.dismissAllTooltips() - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { if itemSetComponentView.hasActiveDeactivateableInput() { itemSetComponentView.deactivateInput() @@ -806,7 +807,7 @@ private final class StoryContainerScreenComponent: Component { self.state?.updated(transition: .immediate) if translation.y < -40.0 { - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { if let activateInputWhileDragging = itemSetComponentView.activateInputWhileDragging() { activateInputWhileDragging() @@ -830,7 +831,7 @@ private final class StoryContainerScreenComponent: Component { self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) self.environment?.controller()?.dismiss() } else if translation.y < -200.0 || (translation.y < -100.0 && velocity.y < -100.0) { - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { if itemSetComponentView.activateInput() { updateState = false @@ -872,7 +873,7 @@ private final class StoryContainerScreenComponent: Component { return } let location = recognizer.location(in: recognizer.view) - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { if currentItemView.hasActiveDeactivateableInput() { currentItemView.deactivateInput() } else { @@ -893,7 +894,7 @@ private final class StoryContainerScreenComponent: Component { } if subview is ItemSetView { - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], itemSetView === subview { + if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], itemSetView === subview { if let result = subview.hitTest(self.convert(point, to: subview), with: event) { return result } @@ -934,7 +935,7 @@ private final class StoryContainerScreenComponent: Component { self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) self.backgroundEffectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - if let transitionIn = self.component?.transitionIn, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if let transitionIn = self.component?.transitionIn, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { itemSetComponentView.animateIn(transitionIn: transitionIn) } @@ -958,7 +959,7 @@ private final class StoryContainerScreenComponent: Component { return } if !value { - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { currentItemView.maybeDisplayReactionTooltip() } } @@ -971,7 +972,7 @@ private final class StoryContainerScreenComponent: Component { func animateOut(completion: @escaping () -> Void) { self.isAnimatingOut = true - if !self.dismissWithoutTransitionOut, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.storyItem.id) { + if !self.dismissWithoutTransitionOut, let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.storyItem.id) { self.state?.updated(transition: .immediate) let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) @@ -991,7 +992,7 @@ private final class StoryContainerScreenComponent: Component { focusedItemPromise.set(.single(nil)) }) } else { - if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let transitionOut = component.transitionOut(slice.peer.id, slice.item.storyItem.id) { + if let component = self.component, let stateValue = self.stateValue, let slice = stateValue.slice, let transitionOut = component.transitionOut(slice.peer.id, slice.item.storyItem.id) { transitionOut.completed() } @@ -1020,10 +1021,10 @@ private final class StoryContainerScreenComponent: Component { private func updateVolumeButtonMonitoring() { if self.volumeButtonsListener == nil { let buttonAction = { [weak self] in - guard let self, let component = self.component else { + guard let self else { return } - guard let slice = component.content.stateValue?.slice else { + guard let slice = self.stateValue?.slice else { return } var isSilentVideo = false @@ -1038,7 +1039,7 @@ private final class StoryContainerScreenComponent: Component { } if isSilentVideo { - if let slice = component.content.stateValue?.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { + if let slice = self.stateValue?.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { currentItemView.displayMutedVideoTooltip() } } else { @@ -1072,7 +1073,7 @@ private final class StoryContainerScreenComponent: Component { return } - if let stateValue = component.content.stateValue, let slice = stateValue.slice { + if let stateValue = self.stateValue, let slice = stateValue.slice { if case .next = direction, slice.nextItemId == nil, (slice.item.position == nil || slice.item.position == slice.totalCount - 1) { if stateValue.nextSlice == nil { environment.controller()?.dismiss() @@ -1194,10 +1195,15 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return } + if self.isAnimatingOut || self.didAnimateOut { + return + } + + let stateValue = component.content.stateValue var focusedItemId: StoryId? var isVideo = false - if let slice = component.content.stateValue?.slice { + if let slice = stateValue?.slice { focusedItemId = StoryId(peerId: slice.peer.id, id: slice.item.storyItem.id) if case .file = slice.item.storyItem.media { isVideo = true @@ -1206,25 +1212,40 @@ private final class StoryContainerScreenComponent: Component { self.focusedItem.set(focusedItemId) self.contentWantsVolumeButtonMonitoring.set(isVideo) - if update { - if component.content.stateValue?.slice == nil { - self.environment?.controller()?.dismiss() - } else { - let startTime = CFAbsoluteTimeGetCurrent() - self.state?.updated(transition: .immediate) - print("update time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + var hasItems = false + if let stateValue { + if stateValue.slice != nil { + hasItems = true } + } + + if !hasItems { + self.dismissWithoutTransitionOut = true + environment.controller()?.dismiss() } else { - DispatchQueue.main.async { [weak self] in - guard let self else { - return + self.stateValue = stateValue + + if update { + if self.stateValue?.slice == nil { + self.environment?.controller()?.dismiss() + } else { + let startTime = CFAbsoluteTimeGetCurrent() + self.state?.updated(transition: .immediate) + print("update time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + } + } else { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.state?.updated(transition: .immediate) } - self.state?.updated(transition: .immediate) } } } self.contentUpdatedDisposable?.dispose() + self.stateValue = component.content.stateValue self.contentUpdatedDisposable = (component.content.updated |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self, let component = self.component else { @@ -1268,7 +1289,7 @@ private final class StoryContainerScreenComponent: Component { } if let pendingNavigationToItemId = self.pendingNavigationToItemId { - if let slice = component.content.stateValue?.slice, slice.peer.id == pendingNavigationToItemId.peerId { + if let slice = self.stateValue?.slice, slice.peer.id == pendingNavigationToItemId.peerId { if slice.item.storyItem.id == pendingNavigationToItemId.id { self.pendingNavigationToItemId = nil } @@ -1315,7 +1336,7 @@ private final class StoryContainerScreenComponent: Component { var currentSlices: [StoryContentContextState.FocusedSlice] = [] var focusedIndex: Int? - if let component = self.component, let stateValue = component.content.stateValue { + if let stateValue = self.stateValue { if let previousSlice = stateValue.previousSlice { currentSlices.append(previousSlice) } @@ -1458,7 +1479,7 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return } - if let stateValue = component.content.stateValue, let slice = stateValue.slice { + if let stateValue = self.stateValue, let slice = stateValue.slice { if slice.nextItemId != nil { component.content.navigate(navigation: .item(.next)) } else if slice.previousItemId != nil { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 095351bd41..0261802de2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -215,7 +215,7 @@ final class StoryItemContentComponent: Component { videoNode.isHidden = true self.videoNode = videoNode - self.addSubview(videoNode.view) + self.insertSubview(videoNode.view, aboveSubview: self.imageView) videoNode.playbackCompleted = { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 4bdf5a82cb..7552cea851 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -444,6 +444,7 @@ public final class StoryItemSetContainerComponent: Component { var reactionContextNode: ReactionContextNode? weak var disappearingReactionContextNode: ReactionContextNode? var displayLikeReactions: Bool = false + var tempReactionsGesture: ContextGesture? var waitingForReactionAnimateOutToLike: MessageReaction.Reaction? weak var standaloneReactionAnimation: StandaloneReactionAnimation? @@ -1656,7 +1657,10 @@ public final class StoryItemSetContainerComponent: Component { externalViews: nil, expandFraction: footerExpandFraction, expandViewStats: { [weak self] in - guard let self else { + guard let self, let component = self.component else { + return + } + if self.viewLists[component.slice.item.storyItem.id] == nil { return } @@ -2025,6 +2029,13 @@ public final class StoryItemSetContainerComponent: Component { self.contextController?.dismiss() self.contextController = nil + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.view.removeFromSuperview() + }) + } + if let inputPanelView = self.inputPanel.view { inputPanelView.layer.animatePosition( from: CGPoint(), @@ -2769,12 +2780,11 @@ public final class StoryItemSetContainerComponent: Component { self.performLikeAction() }, likeOptionsAction: !haveLikeOptions ? nil : { [weak self] sourceView, gesture in - gesture?.cancel() - guard let self else { + gesture?.cancel() return } - self.performLikeOptionsAction(sourceView: sourceView) + self.performLikeOptionsAction(sourceView: sourceView, gesture: gesture) }, inputModeAction: { [weak self] in guard let self else { @@ -3108,20 +3118,7 @@ public final class StoryItemSetContainerComponent: Component { self.isSearchActive = false self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) }, - expandViewStats: { [weak self] in - guard let self else { - return - } - - if self.viewListDisplayState == .hidden { - self.viewListDisplayState = .half - - self.preparingToDisplayViewList = true - self.updateScrolling(transition: .immediate) - self.preparingToDisplayViewList = false - - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - } + expandViewStats: { }, deleteAction: { [weak self] in guard let self, let component = self.component else { @@ -4160,6 +4157,26 @@ public final class StoryItemSetContainerComponent: Component { reactionContextNode.forceDark = true self.reactionContextNode = reactionContextNode + if let tempReactionsGesture = self.tempReactionsGesture { + tempReactionsGesture.externalUpdated = { [weak self] view, point in + guard let self, let view, let reactionContextNode = self.reactionContextNode else { + return + } + let presentationPoint = view.convert(point, to: reactionContextNode.view) + reactionContextNode.highlightGestureMoved(location: presentationPoint, hover: false) + } + tempReactionsGesture.externalEnded = { [weak self] viewAndPoint in + guard let self, let viewAndPoint, let reactionContextNode = self.reactionContextNode else { + return + } + let _ = viewAndPoint + /*let (view, point) = viewAndPoint + let presentationPoint = view.convert(point, to: reactionContextNode.view) + let _ = presentationPoint*/ + reactionContextNode.highlightGestureFinished(performAction: true) + } + } + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in guard let self else { return @@ -4553,7 +4570,7 @@ public final class StoryItemSetContainerComponent: Component { } else if let inputPanelFrameValue { component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrameValue.minY, contentFrame.maxY) } else { - component.externalState.derivedBottomInset = 0.0 + component.externalState.derivedBottomInset = availableSize.height - itemsContainerFrame.maxY } if !"".isEmpty { @@ -5332,12 +5349,14 @@ public final class StoryItemSetContainerComponent: Component { } } - private func performLikeOptionsAction(sourceView: UIView) { + private func performLikeOptionsAction(sourceView: UIView, gesture: ContextGesture?) { HapticFeedback().tap() self.displayLikeReactions = true + self.tempReactionsGesture = gesture self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut))) self.updateIsProgressPaused() + self.tempReactionsGesture = nil } func dismissAllTooltips() { diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 937272a2c9..8b334c9356 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -355,7 +355,7 @@ public final class StoryFooterPanelComponent: Component { } } - self.viewStatsButton.isEnabled = viewCount != 0 + self.viewStatsButton.isEnabled = viewCount != 0 && !component.isChannel var rightContentOffset: CGFloat = availableSize.width - 12.0 diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 085a4a0970..bb5ef10213 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -460,6 +460,18 @@ public final class StoryPeerListComponent: Component { }) } + public func ensureItemVisible(peerId: EnginePeer.Id) { + guard let itemLayout = self.itemLayout else { + return + } + if let index = self.sortedItems.firstIndex(where: { $0.peer.id == peerId }) { + let itemFrame = itemLayout.frame(at: index) + if !self.scrollView.bounds.contains(itemFrame.insetBy(dx: 20.0, dy: 0.0)) { + self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -40.0, dy: 0.0), animated: false) + } + } + } + public func setLoadingItem(peerId: EnginePeer.Id, signal: Signal) { var applyLoadingItem = true self.loadingItemDisposable?.dispose() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e38ad40d13..45c75eb4bd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10012,7 +10012,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + //strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() @@ -13213,7 +13213,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> - if !isScheduledMessages { + if !isScheduledMessages && !peer.isDeleted { buttons = self.context.engine.messages.attachMenuBots() |> map { attachMenuBots in var buttons = availableButtons diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index f73d32a596..c85ae9df19 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -189,7 +189,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { var isRevealed = false var tapped: () -> Void = {} - init(enableAnimations: Bool) { + init(hasImageOverlay: Bool, enableAnimations: Bool) { self.blurredImageNode = TransformImageNode() self.blurredImageNode.contentAnimations = [] @@ -212,7 +212,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { super.init() - self.addSubnode(self.blurredImageNode) + if hasImageOverlay { + self.addSubnode(self.blurredImageNode) + } self.addSubnode(self.dustNode) self.addSubnode(self.buttonNode) @@ -1986,7 +1988,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme) } - if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime { + if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime, Int32(timeout) != viewOnceTimeout { state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true) backgroundColor = messageTheme.mediaDateAndStatusFillColor } else if isSecretMedia, let _ = secretProgressIcon { @@ -2043,7 +2045,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if isSecretMedia { let remainingTime: Int32? - if let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout { + if let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, Int32(timeout) != viewOnceTimeout { if let beginTime = maybeBeginTime { let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime remainingTime = Int32(max(0.0, timeout - elapsedTime)) @@ -2133,7 +2135,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if displaySpoiler { if self.extendedMediaOverlayNode == nil { - let extendedMediaOverlayNode = ExtendedMediaOverlayNode(enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) + let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) extendedMediaOverlayNode.tapped = { [weak self] in self?.internallyVisible = true self?.updateVisibility() diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 4b9246fbca..783a4bd083 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -266,6 +266,8 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openActiveSessions: { + }, performActiveSessionAction: { _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index 3efaf6a169..87b6378e2d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -982,6 +982,29 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition) } + + var removeKeys: [PeerInfoPaneKey] = [] + for (key, paneNode) in self.pendingPanes { + if !availablePanes.contains(key) { + removeKeys.append(key) + paneNode.pane.node.removeFromSupernode() + } + } + for key in removeKeys { + self.pendingPanes.removeValue(forKey: key) + } + removeKeys.removeAll() + + for (key, paneNode) in self.currentPanes { + if !availablePanes.contains(key) { + removeKeys.append(key) + paneNode.node.removeFromSupernode() + } + } + for key in removeKeys { + self.currentPanes.removeValue(forKey: key) + } + if !self.didSetIsReady && data != nil { if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { self.didSetIsReady = true diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 47ad60579d..7aede0fcdc 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -382,6 +382,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } + let target: Stories.PendingTarget + let targetPeerId: EnginePeer.Id + if let sendAsPeerId = options.sendAsPeerId { + target = .peer(sendAsPeerId) + targetPeerId = sendAsPeerId + } else { + target = .myStories + targetPeerId = context.account.peerId + } + storyTarget = target + let completionImpl: () -> Void = { [weak self] in guard let self else { return @@ -393,7 +404,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon |> take(1) |> timeout(0.25, queue: .mainQueue(), alternate: .single(true)) |> deliverOnMainQueue).start(completed: { [weak chatListController] in - chatListController?.scrollToStories() + guard let chatListController else { + return + } + + chatListController.scrollToStories(peerId: targetPeerId) Queue.mainQueue().justDispatch { commit({}) } @@ -405,14 +420,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } - let target: Stories.PendingTarget - if let sendAsPeerId = options.sendAsPeerId { - target = .peer(sendAsPeerId) - } else { - target = .myStories - } - storyTarget = target - if let _ = self.chatListController as? ChatListControllerImpl { switch mediaResult { case let .image(image, dimensions): diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 2e3f0d07d6..78a0d927f6 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1104,7 +1104,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if result { sendEvent(true) } else { - controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: "Allow Sending Messages?", text: "Allow \(controller.botName) to send messages?", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_AllowWriteTitle, text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { sendEvent(false) }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { @@ -1115,16 +1115,23 @@ public final class WebAppController: ViewController, AttachmentContainable { |> deliverOnMainQueue).start(completed: { sendEvent(true) }) - })]), in: .window(.root)) + })], parseMarkdown: true) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + sendEvent(false) + } + } + controller.present(alertController, in: .window(.root)) } }) } fileprivate func shareAccountContact() { - guard let controller = self.controller else { + guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName else { return } + let sendEvent: (Bool) -> Void = { success in var paramsString: String if success { @@ -1135,28 +1142,76 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "phone_requested", data: paramsString) } - let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { - sendEvent(false) - }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in - guard let self else { + let context = self.context + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId), + TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, isBlocked in + guard let self, let controller, let accountPeer else { return } - let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) - |> deliverOnMainQueue).start(next: { [weak self] peer in - if let self, let botId = self.controller?.botId, let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - let _ = enqueueMessages(account: self.context.account, peerId: botId, messages: [ - .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - ]).start() - sendEvent(true) - } - }) - })]) - alertController.dismissed = { byOutsideTap in - if byOutsideTap { - sendEvent(false) + var requiresUnblock = false + if case let .known(value) = isBlocked, value { + requiresUnblock = true } - } - controller.present(alertController, in: .window(.root)) + + let text: String + if requiresUnblock { + text = self.presentationData.strings.WebApp_SharePhoneConfirmationUnblock(botName).string + } else { + text = self.presentationData.strings.WebApp_SharePhoneConfirmation(botName).string + } + + let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_SharePhoneTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + sendEvent(false) + }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in + guard let self, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else { + return + } + + let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [ + .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + ]) + |> mapToSignal { messageIds in + if let maybeMessageId = messageIds.first, let messageId = maybeMessageId { + return context.account.pendingMessageManager.pendingMessageStatus(messageId) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + } else { + return .complete() + } + } + + let sendMessage = { + let _ = (sendMessageSignal + |> deliverOnMainQueue).start(completed: { + sendEvent(true) + }) + } + + if requiresUnblock { + let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false) + |> deliverOnMainQueue).start(completed: { + sendMessage() + }) + } else { + sendMessage() + } + })], parseMarkdown: true) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + sendEvent(false) + } + } + controller.present(alertController, in: .window(.root)) + }) } fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) {