From 6e76fa8bec059dfaebbbe06acf9e1e95a2df438a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 7 Sep 2023 17:45:41 +0400 Subject: [PATCH] Cherry-pick media timer and web app improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 21 + .../Sources/AccountContext.swift | 1 + .../AttachmentTextInputPanelNode.swift | 13 +- .../Sources/AttachmentController.swift | 6 +- .../Sources/AttachmentPanel.swift | 6 +- .../Sources/ChatListSearchMediaNode.swift | 20 +- .../ChatMessageInteractiveMediaBadge.swift | 46 +- ...SendMessageActionSheetControllerNode.swift | 2 +- .../ComponentFlow/Source/Base/Component.swift | 2 +- .../Sources/CounterContollerTitleView.swift | 29 +- submodules/Display/Source/NavigationBar.swift | 4 +- submodules/GalleryUI/BUILD | 2 + .../ChatItemGalleryFooterContentNode.swift | 4 + .../GalleryUI/Sources/GalleryController.swift | 2 + .../Sources/Items/ChatImageGalleryItem.swift | 14 +- .../Items/UniversalVideoGalleryItem.swift | 5 + .../SecretMediaPreviewController.swift | 165 ++++++- .../TGPhotoCaptionInputMixin.h | 6 +- .../TGPhotoPaintStickersContext.h | 9 +- .../Sources/TGCameraController.m | 2 +- .../TGMediaPickerGalleryInterfaceView.m | 42 +- .../Sources/TGMediaPickerModernGalleryMixin.m | 2 +- .../Sources/TGPhotoCaptionInputMixin.m | 33 +- .../Sources/LegacyICloudFilePicker.swift | 3 +- .../Sources/LegacyMediaPickers.swift | 6 +- .../Sources/LocationPickerController.swift | 4 + .../Sources/LegacyMediaPickerGallery.swift | 8 +- .../Sources/MediaPickerGridItem.swift | 10 +- .../Sources/MediaPickerScreen.swift | 108 ++--- .../Sources/MoreButtonNode.swift | 26 +- submodules/RadialStatusNode/BUILD | 1 + .../Sources/RadialStatusIconContentNode.swift | 19 +- .../Sources/RadialStatusNode.swift | 54 ++- ...RadialStatusSecretTimeoutContentNode.swift | 53 ++- .../Account/AccountIntermediateState.swift | 6 +- .../ApiUtils/StoreMessage_Telegram.swift | 4 +- .../State/AccountStateManagementUtils.swift | 14 +- ...ecretChatIncomingDecryptedOperations.swift | 2 +- .../Sources/State/Serialization.swift | 2 +- .../State/UserLimitsConfiguration.swift | 9 +- ...re_AutoremoveTimeoutMessageAttribute.swift | 16 +- .../Data/ConfigurationData.swift | 8 +- .../Messages/AttachMenuBots.swift | 31 +- .../TelegramEngine/Messages/BotWebView.swift | 18 +- ...essageContentAsConsumedInteractively.swift | 34 +- .../Messages/TelegramEngineMessages.swift | 4 +- .../TelegramNotices/Sources/Notices.swift | 22 + .../Resources/PresentationResourceKey.swift | 5 +- .../Resources/PresentationResourcesChat.swift | 22 +- submodules/TelegramUI/BUILD | 1 + .../Sources/AnimatedTextComponent.swift | 13 +- .../Sources/ButtonComponent.swift | 2 +- .../CameraScreen/Sources/CameraScreen.swift | 50 ++- .../ContextReferenceButtonComponent/BUILD | 20 + .../ContextReferenceButtonComponent.swift | 124 ++++++ .../Components/LegacyMessageInputPanel/BUILD | 29 ++ .../Sources/LegacyMessageInputPanel.swift | 376 ++++++++++++++++ .../Sources/MediaEditorScreen.swift | 144 ++++--- .../Sources/MediaToolsScreen.swift | 7 +- .../Sources/StoryPreviewComponent.swift | 7 +- .../MessageInputPanelComponent/BUILD | 5 + .../Sources/InputContextQueries.swift | 102 +++-- .../Sources/MessageInputPanelComponent.swift | 395 +++++++++++++---- .../Sources/TimeoutContentComponent.swift | 137 ++++++ .../Sources/NewSessionInfoScreen.swift | 2 +- .../StoryItemSetContainerComponent.swift | 9 +- ...StoryItemSetContainerViewSendMessage.swift | 4 +- .../Sources/TextFieldComponent.swift | 100 ++++- .../SecretMediaIcon.imageset/Contents.json | 22 - .../SecretMediaIcon@2x.png | Bin 753 -> 0 bytes .../SecretMediaIcon@3x.png | Bin 1138 -> 0 bytes .../SecretMediaOnce.imageset/Contents.json | 12 + .../SecretMediaOnce.imageset/miniplayonce.pdf | 107 +++++ .../SecretMediaPlay.imageset/Contents.json | 12 + .../SecretMediaPlay.imageset/miniplay.pdf | 73 ++++ .../ViewOnce.imageset/Contents.json | 12 + .../ViewOnce.imageset/viewonce_30.pdf | 212 +++++++++ .../AvatarBoost.imageset/AvatarBoost.pdf | 163 +++++++ .../AvatarBoost.imageset/Contents.json | 12 + .../Premium/Boost.imageset/Boost.pdf | 79 ++++ .../Premium/Boost.imageset/Contents.json | 12 + .../BoostChannel.imageset/Contents.json | 12 + .../BoostChannel.imageset/SmallBoost.pdf | 79 ++++ .../Premium/CopyLink.imageset/Contents.json | 12 + .../Premium/CopyLink.imageset/Copy.pdf | 402 ++++++++++++++++++ .../TelegramUI/Sources/AccountContext.swift | 17 +- .../TelegramUI/Sources/ChatController.swift | 27 +- .../ChatMessageAttachedContentNode.swift | 4 +- ...atMessageInteractiveInstantVideoNode.swift | 13 +- .../ChatMessageInteractiveMediaNode.swift | 102 +++-- .../TelegramUI/Sources/GridMessageItem.swift | 8 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 7 +- .../Sources/PeerInfo/PeerInfoData.swift | 9 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 75 +++- .../Sources/PeerInfoGifPaneNode.swift | 8 +- .../Sources/SharedAccountContext.swift | 192 +++++---- .../TooltipUI/Sources/TooltipScreen.swift | 15 +- .../Sources/UndoOverlayControllerNode.swift | 12 +- .../WebUI/Sources/WebAppController.swift | 115 ++++- .../Sources/WebAppTermsAlertController.swift | 302 +++++++++++++ submodules/WebUI/Sources/WebAppWebView.swift | 3 + versions.json | 2 +- 102 files changed, 3940 insertions(+), 644 deletions(-) create mode 100644 submodules/TelegramUI/Components/ContextReferenceButtonComponent/BUILD create mode 100644 submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift create mode 100644 submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD create mode 100644 submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift create mode 100644 submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/TimeoutContentComponent.swift delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@2x.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@3x.png create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/miniplayonce.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/miniplay.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf create mode 100644 submodules/WebUI/Sources/WebAppTermsAlertController.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 254539b666..1ba840db96 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9859,9 +9859,30 @@ Sorry for the inconvenience."; "WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram."; "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."; "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."; + +"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."; + +"Gallery.ViewOncePhotoTooltip" = "This photo can only be viewed once."; +"Gallery.ViewOnceVideoTooltip" = "This video can only be viewed once."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index cf91a69342..bd236d0333 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1025,6 +1025,7 @@ public protocol AccountContext: AnyObject { var animatedEmojiStickers: [String: [StickerPackItem]] { get } + var isPremium: Bool { get } var userLimits: EngineConfiguration.UserLimits { get } func storeSecureIdPassword(password: String) diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 05217f486c..ef2e88af01 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -446,8 +446,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS public var sendPressed: ((NSAttributedString?) -> Void)? public var focusUpdated: ((Bool) -> Void)? public var heightUpdated: ((Bool) -> Void)? + public var timerUpdated: ((NSNumber?) -> Void)? - public func updateLayoutSize(_ size: CGSize, sideInset: CGFloat, animated: Bool) -> CGFloat { + public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { guard let presentationInterfaceState = self.presentationInterfaceState else { return 0.0 } @@ -460,6 +461,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } + public func setTimeout(_ timeout: Int32) { + } + + public func animate(_ view: UIView, frame: CGRect) { + + } + + public func onAnimateOut() { + } + public func dismissInput() { self.ensureUnfocused() } diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index d78ef8bdb4..4b35a35891 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -19,7 +19,7 @@ public enum AttachmentButtonType: Equatable { case location case contact case poll - case app(Peer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile]) + case app(EnginePeer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile]) case gift case standalone @@ -56,7 +56,7 @@ public enum AttachmentButtonType: Equatable { return false } case let .app(lhsPeer, lhsTitle, lhsIcons): - if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, arePeersEqual(lhsPeer, rhsPeer), lhsTitle == rhsTitle, lhsIcons == rhsIcons { + if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, lhsPeer == rhsPeer, lhsTitle == rhsTitle, lhsIcons == rhsIcons { return true } else { return false @@ -1040,7 +1040,7 @@ public class AttachmentController: ViewController { |> deliverOnMainQueue).start(next: { bots in for bot in bots { for (name, file) in bot.icons { - if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer) { + if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) { if case .placeholder = name { let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) if !FileManager.default.fileExists(atPath: path) { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index e81fc2894a..505cd6b429 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -176,7 +176,7 @@ private final class AttachButtonComponent: CombinedComponent { let imageName: String var imageFile: TelegramMediaFile? var animationFile: TelegramMediaFile? - var botPeer: Peer? + var botPeer: EnginePeer? let component = context.component let strings = component.strings @@ -245,7 +245,7 @@ private final class AttachButtonComponent: CombinedComponent { ) } else { var fileReference: FileMediaReference? - if let peer = botPeer.flatMap({ PeerReference($0 )}), let imageFile = imageFile { + if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile { fileReference = .attachBot(peer: peer, media: imageFile) } @@ -1143,7 +1143,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { if case let .app(peer, _, iconFiles) = type { for (name, file) in iconFiles { if [.default, .iOSAnimated, .placeholder].contains(name) { - if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer) { + if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer._asPeer()) { if case .placeholder = name { let account = self.context.account let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) diff --git a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift index 0923c3c18f..d8b21b9fd6 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift @@ -264,18 +264,18 @@ private final class VisualMediaItemNode: ASDisplayNode { if isStreamable { switch status { - case let .Fetching(_, progress): - let progressString = String(format: "%d%%", Int(progress * 100.0)) - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString)) - mediaDownloadState = .compactFetching(progress: 0.0) - case .Local: - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) - case .Remote, .Paused: - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) - mediaDownloadState = .compactRemote + case let .Fetching(_, progress): + let progressString = String(format: "%d%%", Int(progress * 100.0)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil) + mediaDownloadState = .compactFetching(progress: 0.0) + case .Local: + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) + case .Remote, .Paused: + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) + mediaDownloadState = .compactRemote } } else { - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) } strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) diff --git a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift index c97b5b013b..611a6966d5 100644 --- a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift +++ b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift @@ -18,13 +18,13 @@ public enum ChatMessageInteractiveMediaDownloadState: Equatable { } public enum ChatMessageInteractiveMediaBadgeContent: Equatable { - case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, text: NSAttributedString) + case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, text: NSAttributedString, iconName: String?) case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String?, muted: Bool, active: Bool) public static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool { switch lhs { - case let .text(lhsInset, lhsBackgroundColor, lhsForegroundColor, lhsText): - if case let .text(rhsInset, rhsBackgroundColor, rhsForegroundColor, rhsText) = rhs, lhsInset.isEqual(to: rhsInset), lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsText.isEqual(to: rhsText) { + case let .text(lhsInset, lhsBackgroundColor, lhsForegroundColor, lhsText, lhsIconName): + if case let .text(rhsInset, rhsBackgroundColor, rhsForegroundColor, rhsText, rhsIconName) = rhs, lhsInset.isEqual(to: rhsInset), lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsText.isEqual(to: rhsText), lhsIconName == rhsIconName { return true } else { return false @@ -48,6 +48,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { private var previousContentSize: CGSize? private var backgroundNodeColor: UIColor? private var foregroundColor: UIColor? + private var iconName: String? private let backgroundNode: ASImageNode private let durationNode: ASTextNode @@ -107,14 +108,18 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { } switch content { - case let .text(inset, backgroundColor, foregroundColor, text): + case let .text(inset, backgroundColor, foregroundColor, text, iconName): transition = .immediate if self.backgroundNodeColor != backgroundColor { self.backgroundNodeColor = backgroundColor self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor) } - let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: font, .foregroundColor: foregroundColor]) + var textFont = font + if iconName != nil { + textFont = boldFont + } + let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: textFont, .foregroundColor: foregroundColor]) text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in if let _ = attributes[ChatTextInputAttributes.bold] { convertedText.addAttribute(.font, value: boldFont, range: range) @@ -122,12 +127,33 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { } self.durationNode.attributedText = convertedText let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0)) - self.durationNode.frame = CGRect(x: 7.0 + inset, y: 3.0, width: durationSize.width, height: durationSize.height) + self.durationNode.frame = CGRect(x: 7.0 + inset, y: 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height) currentContentSize = CGSize(width: widthForString(text.string) + 14.0 + inset, height: 18.0) - if let iconNode = self.iconNode { - transition.updateTransformScale(node: iconNode, scale: 0.001) - transition.updateAlpha(node: iconNode, alpha: 0.0) + if let iconName { + let iconNode: ASImageNode + if let current = self.iconNode { + iconNode = current + } else { + iconNode = ASImageNode() + self.iconNode = iconNode + self.backgroundNode.addSubnode(iconNode) + } + + if self.foregroundColor != foregroundColor || self.iconName != iconName { + self.foregroundColor = foregroundColor + self.iconName = iconName + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: foregroundColor) + } + transition.updateAlpha(node: iconNode, alpha: 1.0) + transition.updateTransformScale(node: iconNode, scale: 1.0) + + iconNode.frame = CGRect(x: 3.0, y: 2.0, width: 12.0, height: 14.0) + } else { + if let iconNode = self.iconNode { + transition.updateTransformScale(node: iconNode, scale: 0.001) + transition.updateAlpha(node: iconNode, alpha: 0.0) + } } case let .mediaDownload(backgroundColor, foregroundColor, duration, size, muted, active): if self.backgroundNodeColor != backgroundColor { @@ -209,7 +235,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { let durationWidth = widthForString(duration) transition.updatePosition(node: iconNode, position: CGPoint(x: (active ? 42.0 : 7.0) + durationWidth + 4.0 + 7.0, y: (active ? 8.0 : 4.0) + 5.0)) - + if muted { transition.updateAlpha(node: iconNode, alpha: 1.0) transition.updateTransformScale(node: iconNode, scale: 1.0) diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index bd3b05ce38..893d4ce88c 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -242,7 +242,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendSilently, icon: .sendWithoutSound, hasSeparator: true, action: { sendSilently?() })) - if canSendWhenOnline { + if canSendWhenOnline && schedule != nil { contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: .sendWhenOnline, hasSeparator: true, action: { sendWhenOnline?() })) diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift index 71bafb2dd5..7716349577 100644 --- a/submodules/ComponentFlow/Source/Base/Component.swift +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -89,7 +89,7 @@ extension UIView { } open class ComponentState { - var _updated: ((Transition) -> Void)? + open var _updated: ((Transition) -> Void)? var isUpdated: Bool = false public init() { diff --git a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift index 455947e152..17c9e99ac0 100644 --- a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift +++ b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift @@ -32,9 +32,34 @@ public final class CounterContollerTitleView: UIView { } } + private var primaryTextColor: UIColor? + private var secondaryTextColor: UIColor? + + public func updateTextColors(primary: UIColor?, secondary: UIColor?, transition: ContainedViewLayoutTransition) { + self.primaryTextColor = primary + self.secondaryTextColor = secondary + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.snapshotContentTree() { + snapshotView.frame = self.bounds + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + + self.update() + } + private func update() { - self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) - self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor + let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor + self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor) self.accessibilityLabel = self.title.title self.accessibilityValue = self.title.counter diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 06f91b8997..5a4fe019be 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1186,11 +1186,11 @@ open class NavigationBar: ASDisplayNode { transition.updateAlpha(node: self.stripeNode, alpha: alpha, delay: 0.15) } - public func updatePresentationData(_ presentationData: NavigationBarPresentationData) { + public func updatePresentationData(_ presentationData: NavigationBarPresentationData, transition: ContainedViewLayoutTransition = .immediate) { if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings { self.presentationData = presentationData - self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: .immediate) + self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: transition) self.backButtonNode.color = self.presentationData.theme.buttonColor self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 82b0612031..84ce4005a6 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -46,6 +46,8 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", + "//submodules/TooltipUI", + "//submodules/TelegramNotices", ], visibility = [ "//visibility:public", 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 65cd5d82f7..a62933a9d5 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -183,6 +183,7 @@ public func galleryItemForEntry( location: location, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected, + isSecret: isSecret, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, @@ -264,6 +265,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 0aaf8b7d58..2e84933a72 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/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index b6a7ea0300..c615262a11 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2771,6 +2771,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.playbackRatePromise.set(self.playbackRate ?? 1.0) } + public func seekToStart() { + self.videoNode?.seek(0.0) + self.videoNode?.play() + } + override var keyShortcuts: [KeyShortcut] { let strings = self.presentationData.strings diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 26a0fddda9..38be8215c6 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -11,6 +11,8 @@ import RadialStatusNode import ScreenCaptureDetection import AppBundle import LocalizedPeerData +import TooltipUI +import TelegramNotices private func galleryMediaForMedia(media: Media) -> Media? { if let media = media as? TelegramMediaImage { @@ -54,26 +56,34 @@ private func mediaForMessage(message: Message) -> Media? { } private final class SecretMediaPreviewControllerNode: GalleryControllerNode { - private var timeoutNode: RadialStatusNode? + fileprivate var timeoutNode: RadialStatusNode? private var validLayout: (ContainerViewLayout, CGFloat)? - + var beginTimeAndTimeout: (Double, Double)? { didSet { if let (beginTime, timeout) = self.beginTimeAndTimeout { + var beginTime = beginTime if self.timeoutNode == nil { let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.timeoutNode = timeoutNode - var iconImage: UIImage? - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: .white) { - let factor: CGFloat = 0.48 - iconImage = generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) - }) + let icon: RadialStatusNodeState.SecretTimeoutIcon + let timeoutValue = Int32(timeout) + if timeoutValue == viewOnceTimeout { + beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) { + icon = .image(image) + } else { + icon = .flame + } + } else { + icon = .flame } - timeoutNode.transitionToState(.secretTimeout(color: .white, icon: iconImage, beginTime: beginTime, timeout: timeout, sparks: true), completion: {}) + timeoutNode.transitionToState(.secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: true), completion: {}) self.addSubnode(timeoutNode) + + timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside) if let (layout, navigationHeight) = self.validLayout { self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate) @@ -86,6 +96,13 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode { } } + var statusPressed: (UIView) -> Void = { _ in } + @objc private func statusTapGesture() { + if let sourceView = self.timeoutNode?.view { + self.statusPressed(sourceView) + } + } + override func animateIn(animateContent: Bool, useSimpleAnimation: Bool) { super.animateIn(animateContent: animateContent, useSimpleAnimation: useSimpleAnimation) @@ -139,8 +156,12 @@ 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 centralItemAttributesDisposable = DisposableSet(); + private let footerContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() + private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) private var hiddenMediaManagerIndex: Int? @@ -148,6 +169,8 @@ public final class SecretMediaPreviewController: ViewController { private var screenCaptureEventsDisposable: Disposable? + private weak var tooltipController: TooltipScreen? + public init(context: AccountContext, messageId: MessageId) { self.context = context self.messageId = messageId @@ -177,6 +200,15 @@ public final class SecretMediaPreviewController: ViewController { return nil } }) + + self.centralItemAttributesDisposable.add(self.footerContentNode.get().start(next: { [weak self] footerContentNode, _ in + guard let self else { + return + } + self.controllerNode.updatePresentationState({ + $0.withUpdatedFooterContentNode(footerContentNode) + }, transition: .immediate) + })) } required public init(coder aDecoder: NSCoder) { @@ -193,6 +225,7 @@ public final class SecretMediaPreviewController: ViewController { if let tempFile = self.tempFile { TempBox.shared.dispose(tempFile) } + self.centralItemAttributesDisposable.dispose() } @objc func donePressed() { @@ -213,6 +246,12 @@ public final class SecretMediaPreviewController: ViewController { self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction) self.displayNodeDidLoad() + self.controllerNode.statusPressed = { [weak self] _ in + if let self { + self.presentViewOnceTooltip() + } + } + self.controllerNode.statusBar = self.statusBar self.controllerNode.navigationBar = self.navigationBar @@ -231,6 +270,11 @@ public final class SecretMediaPreviewController: ViewController { self.controllerNode.dismiss = { [weak self] in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) + + if let tooltipController = self?.tooltipController { + self?.tooltipController = nil + tooltipController.dismiss() + } } self.controllerNode.beginCustomDismiss = { [weak self] _ in @@ -263,16 +307,20 @@ public final class SecretMediaPreviewController: ViewController { } if let attribute = message.autoclearAttribute { + strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout + if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) } } } else if let attribute = message.autoremoveAttribute { + strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout + if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -284,17 +332,29 @@ 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 { strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout } - if !message.flags.contains(.Incoming) { + if strongSelf.currentNodeMessageIsVideo { + if let node = strongSelf.controllerNode.pager.centralItemNode() { + strongSelf.footerContentNode.set(node.footerContent()) + } + } else if !message.flags.contains(.Incoming) { if let _ = beginTimeAndTimeout { strongSelf.controllerNode.updatePresentationState({ $0.withUpdatedFooterContentNode(nil) @@ -373,9 +433,26 @@ public final class SecretMediaPreviewController: ViewController { self.controllerNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false) } } + + if self.currentNodeMessageIsViewOnce { + let _ = (ApplicationSpecificNotice.incrementViewOnceTooltip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + guard let self else { + return + } + if count < 2 { + self.presentViewOnceTooltip() + } + }) + } } private func dismiss(forceAway: Bool) { + if let tooltipController = self.tooltipController { + self.tooltipController = nil + tooltipController.dismiss() + } + var animatedOutNode = true var animatedOutInterface = false @@ -430,7 +507,15 @@ public final class SecretMediaPreviewController: ViewController { } guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in - self?.dismiss(forceAway: false) + if let self { + if self.currentNodeMessageIsViewOnce { + if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode { + node.seekToStart() + } + } else { + self.dismiss(forceAway: false) + } + } }, present: { _, _ in }) else { self._ready.set(.single(true)) return @@ -452,7 +537,7 @@ public final class SecretMediaPreviewController: ViewController { } if let attribute = message.autoclearAttribute { if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -460,7 +545,7 @@ public final class SecretMediaPreviewController: ViewController { } } else if let attribute = message.autoremoveAttribute { if let countdownBeginTime = attribute.countdownBeginTime { - if let videoDuration = videoDuration { + if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout { beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) } else { beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) @@ -478,12 +563,54 @@ public final class SecretMediaPreviewController: ViewController { if !self.didSetReady { self._ready.set(.single(true)) } - if !self.currentNodeMessageIsVideo { + if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) { self.dismiss() } } } + private func presentViewOnceTooltip() { + guard self.currentNodeMessageIsViewOnce, let sourceView = self.controllerNode.timeoutNode?.view else { + return + } + + if let tooltipController = self.tooltipController { + self.tooltipController = nil + tooltipController.dismiss() + } + + let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize()) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let iconName = "anim_autoremove_on" + let text: String + if self.currentNodeMessageIsVideo { + text = presentationData.strings.Gallery_ViewOnceVideoTooltip + } else { + text = presentationData.strings.Gallery_ViewOncePhotoTooltip + } + + let tooltipController = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .plain(text: text), + balancedTextLayout: true, + constrainWidth: 210.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: .animation(name: iconName, delay: 0.1, tintColor: nil), + location: .point(location, .top), + displayDuration: .default, + inset: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + self.present(tooltipController, in: .window(.root)) + } + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h index 3a155692c6..e420bc3634 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h @@ -22,18 +22,22 @@ @property (nonatomic, copy) void (^panelFocused)(void); @property (nonatomic, copy) void (^finishedWithCaption)(NSAttributedString *caption); @property (nonatomic, copy) void (^keyboardHeightChanged)(CGFloat keyboardHeight, NSTimeInterval duration, NSInteger animationCurve); +@property (nonatomic, copy) void (^timerUpdated)(NSNumber *timeout); - (void)createInputPanelIfNeeded; - (void)beginEditing; - (void)enableDismissal; +- (void)onAnimateOut; + - (void)destroy; @property (nonatomic, strong) NSAttributedString *caption; - (void)setCaption:(NSAttributedString *)caption animated:(bool)animated; - - (void)setCaptionPanelHidden:(bool)hidden animated:(bool)animated; +- (void)setTimeout:(int32_t)timeout; + - (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 0bfdbec4aa..eaf71260ae 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -22,15 +22,22 @@ @property (nonatomic, readonly) UIView * _Nonnull view; +- (void)setTimeout:(int32_t)timeout; + - (NSAttributedString * _Nonnull)caption; - (void)setCaption:(NSAttributedString * _Nullable)caption; - (void)dismissInput; +- (void)animateView:(UIView * _Nonnull)view frame:(CGRect)frame; + +- (void)onAnimateOut; + @property (nonatomic, copy) void(^ _Nullable sendPressed)(NSAttributedString * _Nullable string); @property (nonatomic, copy) void(^ _Nullable focusUpdated)(BOOL focused); @property (nonatomic, copy) void(^ _Nullable heightUpdated)(BOOL animated); +@property (nonatomic, copy) void(^ _Nullable timerUpdated)(NSNumber * _Nullable value); -- (CGFloat)updateLayoutSize:(CGSize)size sideInset:(CGFloat)sideInset animated:(bool)animated; +- (CGFloat)updateLayoutSize:(CGSize)size keyboardHeight:(CGFloat)keyboardHeight sideInset:(CGFloat)sideInset animated:(bool)animated; - (CGFloat)baseHeight; @end diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index 182b89cfd7..6cdb334208 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1568,7 +1568,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus } } - TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSendWhenOnline:false canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder hasTimer:strongSelf->_hasTimer]; + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSendWhenOnline:effectiveHasSchedule canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder hasTimer:strongSelf->_hasTimer]; controller.send = ^{ __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 785952c7c6..08ba0caa6a 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -42,6 +42,28 @@ #import +@interface TGMediaPickerGalleryWrapperView: UIView +{ + +} +@end + +@implementation TGMediaPickerGalleryWrapperView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + __block UIView *result = nil; + [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) { + UIView *hitTestView = [view hitTest:[self convertPoint:point toView:view] withEvent:event]; + if (hitTestView != nil) { + *stop = true; + result = hitTestView; + } + }]; + return result; +} + +@end + @interface TGMediaPickerGalleryInterfaceView () { id _currentItem; @@ -121,7 +143,7 @@ _itemHeaderViews = [[NSMutableArray alloc] init]; _itemFooterViews = [[NSMutableArray alloc] init]; - _wrapperView = [[UIView alloc] initWithFrame:CGRectZero]; + _wrapperView = [[TGMediaPickerGalleryWrapperView alloc] initWithFrame:CGRectZero]; [self addSubview:_wrapperView]; _headerWrapperView = [[UIView alloc] init]; @@ -148,6 +170,8 @@ strongSelf->_portraitToolbarView.doneButton.userInteractionEnabled = false; strongSelf->_landscapeToolbarView.doneButton.userInteractionEnabled = false; strongSelf->_donePressed(strongSelf->_currentItem); + + [strongSelf->_captionMixin onAnimateOut]; }; void(^toolbarDoneLongPressed)(id) = ^(id sender) { @@ -350,6 +374,18 @@ } completion:nil]; }; + _captionMixin.timerUpdated = ^(NSNumber *timeout) { + __strong TGMediaPickerGalleryInterfaceView *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (![strongSelf->_currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)]) + return; + + id galleryEditableItem = (id)strongSelf->_currentItem; + [strongSelf->_editingContext setTimer:timeout forItem:galleryEditableItem.editableMediaItem]; + }; + _captionMixin.stickersContext = stickersContext; [_captionMixin createInputPanelIfNeeded]; @@ -800,6 +836,8 @@ id adjustments = dict[@"adjustments"]; NSNumber *timer = dict[@"timer"]; + [strongSelf->_captionMixin setTimeout:[timer intValue]]; + if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) { TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments; @@ -1285,7 +1323,7 @@ - (void)animateTransitionOutWithDuration:(NSTimeInterval)__unused duration { - + [_captionMixin onAnimateOut]; } - (void)setTransitionOutProgress:(CGFloat)transitionOutProgress manual:(bool)manual diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index b5103bb1f8..c7080a2b7e 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -165,7 +165,7 @@ } } - TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:true canSchedule:effectiveHasSchedule reminder:reminder hasTimer:hasTimer]; + TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:effectiveHasSchedule canSchedule:effectiveHasSchedule reminder:reminder hasTimer:false]; controller.send = ^{ __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil) diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index 98f1744ecb..08d8017a9f 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -87,6 +87,14 @@ [strongSelf updateLayoutWithFrame:strongSelf->_currentFrame edgeInsets:strongSelf->_currentEdgeInsets animated:animated]; }; + _inputPanel.timerUpdated = ^(NSNumber *value) { + __strong TGPhotoCaptionInputMixin *strongSelf = weakSelf; + if (strongSelf.timerUpdated != nil) { + strongSelf.timerUpdated(value); + } + }; + + _inputPanelView = inputPanel.view; _backgroundView = [[UIView alloc] init]; @@ -95,6 +103,10 @@ [parentView addSubview:_inputPanelView]; } +- (void)onAnimateOut { + [_inputPanel onAnimateOut]; +} + - (void)destroy { [_inputPanelView removeFromSuperview]; @@ -129,6 +141,10 @@ [_inputPanel setCaption:caption]; } +- (void)setTimeout:(int32_t)timeout { + [_inputPanel setTimeout:timeout]; +} + - (void)setCaptionPanelHidden:(bool)hidden animated:(bool)__unused animated { _inputPanelView.hidden = hidden; @@ -204,7 +220,7 @@ CGRect frame = _currentFrame; UIEdgeInsets edgeInsets = _currentEdgeInsets; - CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size sideInset:0.0 animated:false]; + CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:false]; [UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{ _inputPanelView.frame = CGRectMake(edgeInsets.left, frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight), frame.size.width, panelHeight); @@ -224,7 +240,7 @@ _currentFrame = frame; _currentEdgeInsets = edgeInsets; - CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size sideInset:0.0 animated:animated]; + CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:_keyboardHeight sideInset:0.0 animated:animated]; CGFloat y = 0.0; if (frame.size.width > frame.size.height && !TGIsPad()) { @@ -238,14 +254,15 @@ backgroundHeight += _keyboardHeight - edgeInsets.bottom; } + CGRect panelFrame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight); + CGRect backgroundFrame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight); + if (animated) { - [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ - _inputPanelView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight); - _backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight); - } completion:nil]; + [_inputPanel animateView:_inputPanelView frame:panelFrame]; + [_inputPanel animateView:_backgroundView frame:backgroundFrame]; } else { - _inputPanelView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight); - _backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight); + _inputPanelView.frame = panelFrame; + _backgroundView.frame = backgroundFrame; } } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift index 375e9ffc9b..07b31c457b 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyICloudFilePicker.swift @@ -54,7 +54,7 @@ public enum LegacyICloudFilePickerMode { } } -public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudFilePickerMode = .default, documentTypes: [String] = ["public.item"], 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?() @@ -86,6 +86,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/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 73ba5c27cc..d7cb6cd1bc 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -459,7 +459,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags) - if let timer = item.timer, timer > 0 && timer <= 60 { + if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } if let spoiler = item.spoiler, spoiler { @@ -504,7 +504,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) var attributes: [MessageAttribute] = [] - if let timer = item.timer, timer > 0 && timer <= 60 { + if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } if let spoiler = item.spoiler, spoiler { @@ -749,7 +749,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes) - if let timer = item.timer, timer > 0 && timer <= 60 { + if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } if let spoiler = item.spoiler, spoiler { 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/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 0850de8c80..7604afc788 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -229,6 +229,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, var effectiveHasSchedule = hasSchedule if let editingContext = editingContext { + if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 { + effectiveHasSchedule = false + } for item in selectionContext.selectedItems() { if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 { effectiveHasSchedule = false @@ -239,6 +242,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, let sendWhenOnlineAvailable: Signal if let peer { + if case .secretChat = peer { + effectiveHasSchedule = false + } sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id) |> take(1) |> map { peerView -> Bool in @@ -265,7 +271,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, |> take(1) |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) - let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) + let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false) let dismissImpl = { [weak model] in model?.dismiss(true, false) dismissAll() diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index e947302ed5..763b1630a3 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -455,11 +455,11 @@ final class MediaPickerGridItemNode: GridItemNode { targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale) } - let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true) - |> then( - assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false) - |> delay(0.03, queue: Queue.concurrentDefaultQueue()) - ) + let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false) +// |> then( +// assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false) +// |> delay(0.03, queue: Queue.concurrentDefaultQueue()) +// ) if stories { self.imageNode.contentUpdated = { [weak self] image in diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 0b09329aa4..6269743010 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -676,68 +676,70 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if !controller.didSetupGroups { controller.didSetupGroups = true - controller.groupsPromise.set( - combineLatest( - self.mediaAssetsContext.fetchAssetsCollections(.album), - self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum) - ) - |> map { albums, smartAlbums -> [MediaGroupItem] in - var collections: [PHAssetCollection] = [] - smartAlbums.enumerateObjects { collection, _, _ in - if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { - collections.append(collection) + Queue.concurrentDefaultQueue().after(0.3) { + controller.groupsPromise.set( + combineLatest( + self.mediaAssetsContext.fetchAssetsCollections(.album), + self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum) + ) + |> map { albums, smartAlbums -> [MediaGroupItem] in + var collections: [PHAssetCollection] = [] + smartAlbums.enumerateObjects { collection, _, _ in + if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { + collections.append(collection) + } } - } - smartAlbums.enumerateObjects { collection, index, _ in - var supportedAlbums: [PHAssetCollectionSubtype] = [ - .smartAlbumBursts, - .smartAlbumPanoramas, - .smartAlbumScreenshots, - .smartAlbumSelfPortraits, - .smartAlbumSlomoVideos, - .smartAlbumTimelapses, - .smartAlbumVideos, - .smartAlbumAllHidden - ] - if #available(iOS 11, *) { - supportedAlbums.append(.smartAlbumAnimated) - supportedAlbums.append(.smartAlbumDepthEffect) - supportedAlbums.append(.smartAlbumLivePhotos) + smartAlbums.enumerateObjects { collection, index, _ in + var supportedAlbums: [PHAssetCollectionSubtype] = [ + .smartAlbumBursts, + .smartAlbumPanoramas, + .smartAlbumScreenshots, + .smartAlbumSelfPortraits, + .smartAlbumSlomoVideos, + .smartAlbumTimelapses, + .smartAlbumVideos, + .smartAlbumAllHidden + ] + if #available(iOS 11, *) { + supportedAlbums.append(.smartAlbumAnimated) + supportedAlbums.append(.smartAlbumDepthEffect) + supportedAlbums.append(.smartAlbumLivePhotos) + } + if supportedAlbums.contains(collection.assetCollectionSubtype) { + let result = PHAsset.fetchAssets(in: collection, options: nil) + if result.count > 0 { + collections.append(collection) + } + } } - if supportedAlbums.contains(collection.assetCollectionSubtype) { + albums.enumerateObjects(options: [.reverse]) { collection, _, _ in let result = PHAsset.fetchAssets(in: collection, options: nil) if result.count > 0 { collections.append(collection) } } - } - albums.enumerateObjects(options: [.reverse]) { collection, _, _ in - let result = PHAsset.fetchAssets(in: collection, options: nil) - if result.count > 0 { - collections.append(collection) - } - } - - var items: [MediaGroupItem] = [] - for collection in collections { - let result = PHAsset.fetchAssets(in: collection, options: nil) - let firstItem: PHAsset? - if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { - firstItem = result.lastObject - } else { - firstItem = result.firstObject - } - items.append( - MediaGroupItem( - collection: collection, - firstItem: firstItem, - count: result.count + + var items: [MediaGroupItem] = [] + for collection in collections { + let result = PHAsset.fetchAssets(in: collection, options: nil) + let firstItem: PHAsset? + if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { + firstItem = result.lastObject + } else { + firstItem = result.firstObject + } + items.append( + MediaGroupItem( + collection: collection, + firstItem: firstItem, + count: result.count + ) ) - ) + } + return items } - return items - } - ) + ) + } } } else if case .notDetermined = mediaAccess, !self.requestedMediaAccess { self.requestedMediaAccess = true diff --git a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift index 1c14ba94cc..ca41aeb431 100644 --- a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift +++ b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift @@ -74,12 +74,36 @@ public final class MoreButtonNode: ASDisplayNode { private let buttonNode: HighlightableButtonNode public let iconNode: MoreIconNode + private var color: UIColor? + public var theme: PresentationTheme { didSet { - self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor + self.update() } } + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + self.color = color + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.iconNode.view.snapshotContentTree() { + snapshotView.frame = self.iconNode.frame + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + self.update() + } + + private func update() { + let color = self.color ?? self.theme.rootController.navigationBar.buttonColor + self.iconNode.customColor = color + } + public init(theme: PresentationTheme) { self.theme = theme diff --git a/submodules/RadialStatusNode/BUILD b/submodules/RadialStatusNode/BUILD index 264f6f4f6d..5fb0386325 100644 --- a/submodules/RadialStatusNode/BUILD +++ b/submodules/RadialStatusNode/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/LegacyComponents:LegacyComponents", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ManagedAnimationNode:ManagedAnimationNode" ], visibility = [ "//visibility:public", diff --git a/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift index d0b5f9e2af..0d1510195a 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusIconContentNode.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit enum RadialStatusIcon { case custom(UIImage) + case timeout case play(UIColor) case pause(UIColor) } @@ -22,14 +23,28 @@ private final class RadialStatusIconContentNodeParameters: NSObject { final class RadialStatusIconContentNode: RadialStatusContentNode { private let icon: RadialStatusIcon + private var animationNode: FireIconNode? + init(icon: RadialStatusIcon, synchronous: Bool) { self.icon = icon super.init() self.displaysAsynchronously = !synchronous - self.isLayerBacked = true +// self.isLayerBacked = true self.isOpaque = false + + if case .timeout = icon { + let animationNode = FireIconNode() + self.animationNode = animationNode + self.addSubnode(animationNode) + } + } + + override func layout() { + super.layout() + + self.animationNode?.frame = CGRect(x: 6.0, y: 2.0, width: 36.0, height: 36.0) } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { @@ -48,6 +63,8 @@ final class RadialStatusIconContentNode: RadialStatusContentNode { if let parameters = parameters as? RadialStatusIconContentNodeParameters { let diameter = min(bounds.size.width, bounds.size.height) switch parameters.icon { + case .timeout: + break case let .play(color): context.setFillColor(color.cgColor) diff --git a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift index 28670b84b2..1b7d5e95dd 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift @@ -4,6 +4,35 @@ import AsyncDisplayKit import Display public enum RadialStatusNodeState: Equatable { + public enum SecretTimeoutIcon: Equatable { + case none + case image(UIImage) + case flame + + public static func ==(lhs: SecretTimeoutIcon, rhs: SecretTimeoutIcon) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .image(lhsImage): + if case let .image(rhsImage) = rhs, lhsImage === rhsImage { + return true + } else { + return false + } + case .flame: + if case .flame = rhs { + return true + } else { + return false + } + } + } + } + case none case download(UIColor) case play(UIColor) @@ -12,7 +41,8 @@ public enum RadialStatusNodeState: Equatable { case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?) case check(UIColor) case customIcon(UIImage) - case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double, sparks: Bool) + case staticTimeout + case secretTimeout(color: UIColor, icon: SecretTimeoutIcon, beginTime: Double, timeout: Double, sparks: Bool) public static func ==(lhs: RadialStatusNodeState, rhs: RadialStatusNodeState) -> Bool { switch lhs { @@ -64,8 +94,14 @@ public enum RadialStatusNodeState: Equatable { } else { return false } + case .staticTimeout: + if case .staticTimeout = rhs { + return true + } else { + return false + } case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout, lhsSparks): - if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon === rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks { + if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon == rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks { return true } else { return false @@ -123,8 +159,14 @@ public enum RadialStatusNodeState: Equatable { } else { return false } + case .staticTimeout: + if case .staticTimeout = rhs{ + return true + } else { + return false + } case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout, lhsSparks): - if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon === rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks { + if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon == rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks { return true } else { return false @@ -179,6 +221,8 @@ public enum RadialStatusNodeState: Equatable { node.progress = value return node } + case .staticTimeout: + return RadialStatusIconContentNode(icon: .timeout, synchronous: synchronous) case let .secretTimeout(color, icon, beginTime, timeout, sparks): return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks) } @@ -188,7 +232,9 @@ public enum RadialStatusNodeState: Equatable { public final class RadialStatusNode: ASControlNode { public var backgroundNodeColor: UIColor { didSet { - self.transitionToBackgroundColor(self.state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, synchronous: false, completion: {}) + if self.backgroundNodeColor != oldValue { + self.transitionToBackgroundColor(self.state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, synchronous: false, completion: {}) + } } } diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index ac5fcf9e02..bd2ec3b6ff 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import LegacyComponents +import ManagedAnimationNode private struct ContentParticle { var position: CGPoint @@ -24,12 +25,12 @@ private struct ContentParticle { private final class RadialStatusSecretTimeoutContentNodeParameters: NSObject { let color: UIColor - let icon: UIImage? + let icon: RadialStatusNodeState.SecretTimeoutIcon let progress: CGFloat let sparks: Bool let particles: [ContentParticle] - init(color: UIColor, icon: UIImage?, progress: CGFloat, sparks: Bool, particles: [ContentParticle]) { + init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle]) { self.color = color self.icon = icon self.progress = progress @@ -47,15 +48,17 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { private let beginTime: Double private let timeout: Double - private let icon: UIImage? + private let icon: RadialStatusNodeState.SecretTimeoutIcon private let sparks: Bool private var progress: CGFloat = 0.0 private var particles: [ContentParticle] = [] + private let animationNode = FireIconNode() + private var displayLink: CADisplayLink? - init(color: UIColor, beginTime: Double, timeout: Double, icon: UIImage?, sparks: Bool) { + init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool) { self.color = color self.beginTime = beginTime self.timeout = timeout @@ -65,7 +68,6 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { super.init() self.isOpaque = false - self.isLayerBacked = true class DisplayLinkProxy: NSObject { weak var target: RadialStatusSecretTimeoutContentNode? @@ -81,6 +83,10 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink?.isPaused = true self.displayLink?.add(to: RunLoop.main, forMode: .common) + + if case .flame = icon { + self.addSubnode(self.animationNode) + } } deinit { @@ -89,6 +95,15 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { override func layout() { super.layout() + + 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) { @@ -118,7 +133,11 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - self.progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout)) + var progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout)) + if self.timeout == 0x7fffffff { + progress = 0.0 + } + self.progress = progress if self.sparks { let lineWidth: CGFloat = 1.75 @@ -187,7 +206,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters { - if let icon = parameters.icon, let iconImage = icon.cgImage { + var drawArc = true + if case let .image(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) @@ -195,6 +215,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { context.translateBy(x: -imageRect.midX, y: -imageRect.midY) context.draw(iconImage, in: imageRect) context.restoreGState() + + drawArc = false } let lineWidth: CGFloat @@ -217,10 +239,12 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { let startAngle: CGFloat = -CGFloat.pi / 2.0 let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress - let path = CGMutablePath() - path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) - context.addPath(path) - context.strokePath() + if drawArc { + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + } for particle in parameters.particles { let size: CGFloat = 1.3 @@ -231,3 +255,10 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { } } +final class FireIconNode: ManagedAnimationNode { + init() { + super.init(size: CGSize(width: 100.0, height: 100.0)) + + self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0)) + } +} diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 15a99e5cd8..ddecfa37d3 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -95,7 +95,7 @@ enum AccountStateMutationOperation { case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation) case UpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool) case UpdatePinnedTopicOrder(peerId: PeerId, threadIds: [Int64]) - case ReadMessageContents((PeerId?, [Int32])) + case ReadMessageContents(peerIdsAndMessageIds: (PeerId?, [Int32]), date: Int32?) case UpdateMessageImpressionCount(MessageId, Int32) case UpdateMessageForwardsCount(MessageId, Int32) case UpdateInstalledStickerPacks(AccountStateUpdateStickerPacksOperation) @@ -574,8 +574,8 @@ struct AccountMutableState { self.addOperation(.UpdatePinnedTopicOrder(peerId: peerId, threadIds: threadIds)) } - mutating func addReadMessagesContents(_ peerIdsAndMessageIds: (PeerId?, [Int32])) { - self.addOperation(.ReadMessageContents(peerIdsAndMessageIds)) + mutating func addReadMessagesContents(_ peerIdsAndMessageIds: (PeerId?, [Int32]), date: Int32?) { + self.addOperation(.ReadMessageContents(peerIdsAndMessageIds: peerIdsAndMessageIds, date: date)) } mutating func addUpdateMessageImpressionCount(id: MessageId, count: Int32) { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index c82d26b53b..586db956a3 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -9,11 +9,11 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], var hasUnseenReactions = false for attribute in attributes { if let timerAttribute = attribute as? AutoclearTimeoutMessageAttribute { - if timerAttribute.timeout > 0 && timerAttribute.timeout <= 60 { + if timerAttribute.timeout > 0 && (timerAttribute.timeout <= 60 || timerAttribute.timeout == viewOnceTimeout) { isSecret = true } } else if let timerAttribute = attribute as? AutoremoveTimeoutMessageAttribute { - if timerAttribute.timeout > 0 && timerAttribute.timeout <= 60 { + if timerAttribute.timeout > 0 && (timerAttribute.timeout <= 60 || timerAttribute.timeout == viewOnceTimeout) { isSecret = true } } else if let mentionAttribute = attribute as? ConsumablePersonalMentionMessageAttribute { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 88d8e87f52..c50f3d8657 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1479,11 +1479,11 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: case let .updateChannelPinnedTopic(flags, channelId, topicId): let isPinned = (flags & (1 << 0)) != 0 updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned) - case let .updateReadMessagesContents(_, messages, _, _, _): - updatedState.addReadMessagesContents((nil, messages)) + case let .updateReadMessagesContents(_, messages, _, _, date): + updatedState.addReadMessagesContents((nil, messages), date: date) case let .updateChannelReadMessagesContents(_, channelId, topMsgId, messages): let _ = topMsgId - updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), messages)) + updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), messages), date: nil) case let .updateChannelMessageViews(channelId, id, views): updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: id), count: views) /*case let .updateChannelMessageForwards(channelId, id, forwards): @@ -2929,7 +2929,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo }, pinned: (flags & (1 << 0)) != 0) case let .updateChannelReadMessagesContents(_, _, topMsgId, messages): let _ = topMsgId - updatedState.addReadMessagesContents((peer.id, messages)) + updatedState.addReadMessagesContents((peer.id, messages), date: nil) case let .updateChannelMessageViews(_, id, views): updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views) case let .updateChannelWebPage(_, apiWebpage, _, _): @@ -4181,16 +4181,16 @@ func replayFinalState( transaction.setPeerPinnedThreads(peerId: peerId, threadIds: currentThreadIds) case let .UpdatePinnedTopicOrder(peerId, threadIds): transaction.setPeerPinnedThreads(peerId: peerId, threadIds: threadIds) - case let .ReadMessageContents(peerIdAndMessageIds): + case let .ReadMessageContents(peerIdAndMessageIds, date): let (peerId, messageIds) = peerIdAndMessageIds if let peerId = peerId { for id in messageIds { - markMessageContentAsConsumedRemotely(transaction: transaction, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)) + markMessageContentAsConsumedRemotely(transaction: transaction, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), consumeDate: date) } } else { for messageId in transaction.messageIdsForGlobalIds(messageIds) { - markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId) + markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId, consumeDate: date) } } case let .UpdateMessageImpressionCount(id, count): diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index c6030ec173..1151ec65f2 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -307,7 +307,7 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption } } for messageId in messageIds { - markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId) + markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId, consumeDate: nil) } default: break diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index b4542e8af7..b0931e94c7 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 162 + return 163 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index db5138cd28..bf24347c6f 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -21,6 +21,7 @@ public struct UserLimitsConfiguration: Equatable { public let maxExpiringStoriesCount: Int32 public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 + public let maxStoriesSuggestedReactions: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -42,7 +43,8 @@ public struct UserLimitsConfiguration: Equatable { maxStoryCaptionLength: 200, maxExpiringStoriesCount: 3, maxStoriesWeeklyCount: 7, - maxStoriesMonthlyCount: 30 + maxStoriesMonthlyCount: 30, + maxStoriesSuggestedReactions: 1 ) } @@ -65,7 +67,8 @@ public struct UserLimitsConfiguration: Equatable { maxStoryCaptionLength: Int32, maxExpiringStoriesCount: Int32, maxStoriesWeeklyCount: Int32, - maxStoriesMonthlyCount: Int32 + maxStoriesMonthlyCount: Int32, + maxStoriesSuggestedReactions: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -86,6 +89,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxExpiringStoriesCount = maxExpiringStoriesCount self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount + self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions } } @@ -129,5 +133,6 @@ extension UserLimitsConfiguration { self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount) self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount) self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount) + self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift index 77e1eec8dc..b8c99a0b3f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AutoremoveTimeoutMessageAttribute.swift @@ -1,6 +1,8 @@ import Foundation import Postbox +public let viewOnceTimeout: Int32 = 0x7fffffff + public class AutoremoveTimeoutMessageAttribute: MessageAttribute { public let timeout: Int32 public let countdownBeginTime: Int32? @@ -54,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 } @@ -65,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 } @@ -124,7 +134,7 @@ public extension Message { guard let timeout = self.minAutoremoveOrClearTimeout else { return false } - if timeout > 1 * 60 { + if timeout > 1 * 60 && timeout != viewOnceTimeout { return false } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 7859ddf79f..dad1f563c4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -55,6 +55,7 @@ public enum EngineConfiguration { public let maxExpiringStoriesCount: Int32 public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 + public let maxStoriesSuggestedReactions: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -79,7 +80,8 @@ public enum EngineConfiguration { maxStoryCaptionLength: Int32, maxExpiringStoriesCount: Int32, maxStoriesWeeklyCount: Int32, - maxStoriesMonthlyCount: Int32 + maxStoriesMonthlyCount: Int32, + maxStoriesSuggestedReactions: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -100,6 +102,7 @@ public enum EngineConfiguration { self.maxExpiringStoriesCount = maxExpiringStoriesCount self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount + self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions } } } @@ -155,7 +158,8 @@ public extension EngineConfiguration.UserLimits { maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength, maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount, maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount, - maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount + maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount, + maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 98608c876d..d77f5b2ab1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -52,6 +52,9 @@ public final class AttachMenuBots: Equatable, Codable { public static let hasSettings = Flags(rawValue: 1 << 0) public static let requiresWriteAccess = Flags(rawValue: 1 << 1) + public static let showInAttachMenu = Flags(rawValue: 1 << 2) + public static let showInSettings = Flags(rawValue: 1 << 3) + public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4) } public struct PeerFlags: OptionSet, Codable { @@ -323,6 +326,15 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n if (apiFlags & (1 << 2)) != 0 { flags.insert(.requiresWriteAccess) } + if (apiFlags & (1 << 3)) != 0 { + flags.insert(.showInAttachMenu) + } + if (apiFlags & (1 << 4)) != 0 { + flags.insert(.showInSettings) + } + if (apiFlags & (1 << 5)) != 0 { + flags.insert(.showInSettingsDisclaimer) + } resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags)) } } @@ -427,13 +439,13 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, } public struct AttachMenuBot { - public let peer: Peer + public let peer: EnginePeer public let shortName: String public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] public let peerTypes: AttachMenuBots.Bot.PeerFlags public let flags: AttachMenuBots.Bot.Flags - init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { + init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { self.peer = peer self.shortName = shortName self.icons = icons @@ -450,7 +462,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr var resultBots: [AttachMenuBot] = [] for bot in cachedBots { if let peer = transaction.getPeer(bot.peerId) { - resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) + resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) } } return resultBots @@ -465,7 +477,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network return postbox.transaction { transaction -> Signal in if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots { if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) { - return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) + return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) } } @@ -526,7 +538,16 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network if (apiFlags & (1 << 2)) != 0 { flags.insert(.requiresWriteAccess) } - return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, flags: flags)) + if (apiFlags & (1 << 3)) != 0 { + flags.insert(.showInAttachMenu) + } + if (apiFlags & (1 << 4)) != 0 { + flags.insert(.showInSettings) + } + if (apiFlags & (1 << 5)) != 0 { + flags.insert(.showInSettingsDisclaimer) + } + return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags)) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 82b74778f9..9683f3d5a6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -10,11 +10,17 @@ private let botWebViewPlatform = "macos" private let botWebViewPlatform = "ios" #endif +public enum RequestSimpleWebViewSource { + case generic + case inline + case settings +} + public enum RequestSimpleWebViewError { case generic } -func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal { +func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) @@ -28,8 +34,16 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if let _ = serializedThemeParams { flags |= (1 << 0) } - if inline { + switch source { + case .inline: flags |= (1 << 1) + case .settings: + flags |= (1 << 2) + default: + break + } + if let _ = url { + flags |= (1 << 3) } return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestSimpleWebViewError in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift index 1916744a13..76d2aacf95 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) @@ -160,7 +160,7 @@ func _internal_markReactionsAsSeenInteractively(postbox: Postbox, messageId: Mes } } -func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: MessageId) { +func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: MessageId, consumeDate: Int32?) { if let message = transaction.getMessage(messageId) { var updateMessage = false var updatedAttributes = message.attributes @@ -184,35 +184,41 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M } let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + let countdownBeginTime = consumeDate ?? timestamp + for i in 0 ..< updatedAttributes.count { if let attribute = updatedAttributes[i] as? AutoremoveTimeoutMessageAttribute { if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia { - updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: timestamp) + updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime) updateMessage = true - + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { } else { - for i in 0 ..< updatedMedia.count { - if let _ = updatedMedia[i] as? TelegramMediaImage { - updatedMedia[i] = TelegramMediaExpiredContent(data: .image) - } else if let _ = updatedMedia[i] as? TelegramMediaFile { - updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout { + for i in 0 ..< updatedMedia.count { + if let _ = updatedMedia[i] as? TelegramMediaImage { + updatedMedia[i] = TelegramMediaExpiredContent(data: .image) + } else if let _ = updatedMedia[i] as? TelegramMediaFile { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } } } } else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute { if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia { - updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: timestamp) + updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime) updateMessage = true if message.id.peerId.namespace == Namespaces.Peer.SecretChat { } else { for i in 0 ..< updatedMedia.count { - if let _ = updatedMedia[i] as? TelegramMediaImage { - updatedMedia[i] = TelegramMediaExpiredContent(data: .image) - } else if let _ = updatedMedia[i] as? TelegramMediaFile { - updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout { + if let _ = updatedMedia[i] as? TelegramMediaImage { + updatedMedia[i] = TelegramMediaExpiredContent(data: .image) + } else if let _ = updatedMedia[i] as? TelegramMediaFile { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 52051d9e8d..412728c0f2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -496,8 +496,8 @@ public extension TelegramEngine { return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId) } - public func requestSimpleWebView(botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal { - return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, inline: inline, themeParams: themeParams) + public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams) } public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, allowWrite: Bool) -> Signal { diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index be331d25db..19112f742b 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -179,6 +179,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case displayChatListArchiveTooltip = 45 case displayStoryReactionTooltip = 46 case storyStealthModeReplyCount = 47 + case viewOnceTooltip = 48 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -424,6 +425,10 @@ private struct ApplicationSpecificNoticeKeys { static func storyStealthModeReplyCount() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.storyStealthModeReplyCount.key) } + + static func viewOnceTooltip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.viewOnceTooltip.key) + } } public struct ApplicationSpecificNotice { @@ -1611,4 +1616,21 @@ public struct ApplicationSpecificNotice { } |> ignoreValues } + + public static func incrementViewOnceTooltip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.viewOnceTooltip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.viewOnceTooltip(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 77f6f18dca..7b0736df4a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -133,10 +133,7 @@ public enum PresentationResourceKey: Int32 { case chatMediaConsumableContentIcon case chatBubbleMediaOverlayControlSecret - - case chatBubbleSecretMediaIcon - case chatBubbleSecretMediaCompactIcon - + case chatInstantVideoWithWallpaperBackgroundImage case chatInstantVideoWithoutWallpaperBackgroundImage diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index c26414e8e6..9e38d8bdd6 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -164,27 +164,7 @@ public struct PresentationResourcesChat { return generateFilledCircleImage(diameter: 4.0, color: theme.chat.message.mediaDateAndStatusTextColor) }) } - - public static func chatBubbleSecretMediaIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleSecretMediaIcon.rawValue, { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.message.mediaOverlayControlColors.foregroundColor) - }) - } - - public static func chatBubbleSecretMediaCompactIcon(_ theme: PresentationTheme) -> UIImage? { - return theme.image(PresentationResourceKey.chatBubbleSecretMediaCompactIcon.rawValue, { theme in - if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.message.mediaOverlayControlColors.foregroundColor) { - let factor: CGFloat = 0.6 - return generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) - }) - } else { - return nil - } - }) - } - + public static func chatInstantVideoBackgroundImage(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? { let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatInstantVideoWithoutWallpaperBackgroundImage : PresentationResourceKey.chatInstantVideoWithWallpaperBackgroundImage return theme.image(key.rawValue, { theme in diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 794b2bfcdf..a1513d733d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -379,6 +379,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerReportScreen", "//submodules/Utils/VolumeButtons", "//submodules/ChatContextQuery", + "//submodules/TelegramUI/Components/LegacyMessageInputPanel", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index 130c93133c..cf7766a76a 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -7,7 +7,7 @@ public final class AnimatedTextComponent: Component { public struct Item: Equatable { public enum Content: Equatable { case text(String) - case number(Int) + case number(Int, minDigits: Int) } public var id: AnyHashable @@ -86,11 +86,16 @@ public final class AnimatedTextComponent: Component { } else { itemText = text.map(String.init) } - case let .number(value): + case let .number(value, minDigits): + var valueText: String = "\(value)" + while valueText.count < minDigits { + valueText.insert("0", at: valueText.startIndex) + } + if item.isUnbreakable { - itemText = ["\(value)"] + itemText = [valueText] } else { - itemText = "\(value)".map(String.init) + itemText = valueText.map(String.init) } } var index = 0 diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 5e46d79267..e70ec78bdd 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -184,7 +184,7 @@ public final class ButtonTextContentComponent: Component { font: Font.semibold(15.0), color: component.badgeForeground, items: [ - AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge)) + AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0)) ] )) )), diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 3b1f0ef465..12ecfd9e54 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1422,6 +1422,7 @@ public class CameraScreen: ViewController { self.authorizationStatusDisposables.dispose() } + private var panGestureRecognizer: UIPanGestureRecognizer? private var pipPanGestureRecognizer: UIPanGestureRecognizer? override func didLoad() { super.didLoad() @@ -1435,6 +1436,7 @@ public class CameraScreen: ViewController { let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) panGestureRecognizer.delegate = self panGestureRecognizer.maximumNumberOfTouches = 1 + self.panGestureRecognizer = panGestureRecognizer self.previewContainerView.addGestureRecognizer(panGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) @@ -1464,6 +1466,7 @@ public class CameraScreen: ViewController { }) } + fileprivate var captureStartTimestamp: Double? private func setupCamera() { guard self.camera == nil else { return @@ -1573,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 @@ -1598,6 +1602,8 @@ public class CameraScreen: ViewController { return false } return self.additionalPreviewContainerView.frame.contains(location) + } else if gestureRecognizer === self.panGestureRecognizer { + return true } return self.hasAppeared } @@ -1628,7 +1634,7 @@ public class CameraScreen: ViewController { case .changed: if case .none = self.cameraState.recording { if case .compact = layout.metrics.widthClass { - if translation.x < -10.0 || self.isDismissing { + if (translation.x < -10.0 || self.isDismissing) && self.hasAppeared { self.isDismissing = true let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.updateTransitionProgress(transitionFraction, transition: .immediate) @@ -1640,11 +1646,13 @@ public class CameraScreen: ViewController { } } case .ended, .cancelled: - let velocity = gestureRecognizer.velocity(in: self.view) - let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width - controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) - - self.isDismissing = false + if self.isDismissing { + let velocity = gestureRecognizer.velocity(in: self.view) + let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width + controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) + + self.isDismissing = false + } default: break } @@ -1757,7 +1765,7 @@ public class CameraScreen: ViewController { self.backgroundView.alpha = 1.0 }) - if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + if let layout = self.validLayout, layout.metrics.isTablet { self.controller?.statusBar.updateStatusBarStyle(.Hide, animated: true) } @@ -2075,12 +2083,7 @@ public class CameraScreen: ViewController { let isFirstTime = self.validLayout == nil self.validLayout = layout - let isTablet: Bool - if case .regular = layout.metrics.widthClass { - isTablet = true - } else { - isTablet = false - } + let isTablet = layout.metrics.isTablet var topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let previewSize: CGSize @@ -2514,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 @@ -2615,7 +2629,7 @@ public class CameraScreen: ViewController { self.node.camera?.stopCapture(invalidate: true) self.isDismissed = true if animated { - if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + if let layout = self.validLayout, layout.metrics.isTablet { self.statusBar.updateStatusBarStyle(.Ignore, animated: true) self.node.animateOut(completion: { self.dismiss(animated: false) @@ -2637,7 +2651,7 @@ public class CameraScreen: ViewController { } public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { - if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + if let layout = self.validLayout, layout.metrics.isTablet { return } @@ -2683,7 +2697,7 @@ public class CameraScreen: ViewController { } public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { - if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + if let layout = self.validLayout, layout.metrics.isTablet { return } if dismissing { 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..08c88c2379 --- /dev/null +++ b/submodules/TelegramUI/Components/ContextReferenceButtonComponent/Sources/ContextReferenceButtonComponent.swift @@ -0,0 +1,124 @@ +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.buttonView.allowsGroupOpacity = true + 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/BUILD b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD new file mode 100644 index 0000000000..9ffc71d6c7 --- /dev/null +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LegacyMessageInputPanel", + module_name = "LegacyMessageInputPanel", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/AccountContext", + "//submodules/LegacyComponents", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/ContextUI", + "//submodules/TooltipUI", + "//submodules/TelegramUI/Components/MessageInputPanelComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift new file mode 100644 index 0000000000..86018480df --- /dev/null +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -0,0 +1,376 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import LegacyComponents +import Display +import TelegramCore +import Postbox +import SwiftSignalKit +import AccountContext +import LegacyComponents +import ComponentFlow +import MessageInputPanelComponent +import TelegramPresentationData +import ContextUI +import TooltipUI + +public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { + private let context: AccountContext + private let chatLocation: ChatLocation + private let present: (ViewController) -> Void + private let presentInGlobalOverlay: (ViewController) -> Void + + private let state = ComponentState() + private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() + private let inputPanel = ComponentView() + + private var currentTimeout: Int32? + private var currentIsEditing = false + private var currentHeight: CGFloat? + + private let hapticFeedback = HapticFeedback() + + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)? + + public init( + context: AccountContext, + chatLocation: ChatLocation, + present: @escaping (ViewController) -> Void, + presentInGlobalOverlay: @escaping (ViewController) -> Void + ) { + self.context = context + self.chatLocation = chatLocation + self.present = present + self.presentInGlobalOverlay = presentInGlobalOverlay + + super.init() + + self.state._updated = { [weak self] transition in + if let self { + self.update(transition: transition.containedViewLayoutTransition) + } + } + } + + public var sendPressed: ((NSAttributedString?) -> Void)? + public var focusUpdated: ((Bool) -> Void)? + public var heightUpdated: ((Bool) -> Void)? + public var timerUpdated: ((NSNumber?) -> Void)? + + public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false) + } + + public func caption() -> NSAttributedString { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(caption) = view.getSendMessageInput() { + return caption + } else { + return NSAttributedString() + } + } + + 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: sendMessageInput, updateState: true) + } else { + self.scheduledMessageInput = sendMessageInput + } + } + + public func animate(_ view: UIView, frame: CGRect) { + let transition = Transition.spring(duration: 0.4) + transition.setFrame(view: view, frame: frame) + } + + public func setTimeout(_ timeout: Int32) { + self.dismissTimeoutTooltip() + var timeout: Int32? = timeout + if timeout == 0 { + timeout = nil + } + self.currentTimeout = timeout + } + + public func dismissInput() { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + view.deactivateInput() + } + } + + public func onAnimateOut() { + self.tooltipController?.dismiss() + } + + public func baseHeight() -> CGFloat { + return 52.0 + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func update(transition: ContainedViewLayoutTransition) { + if let (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, metrics: metrics, isMediaInputExpanded: false) + } + } + + public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + let previousLayout = self.validLayout + self.validLayout = (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) + + var transition = transition + if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated { + transition = .animated(duration: 0.4, curve: .spring) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let theme = defaultDarkColorPresentationTheme + + var timeoutValue: String + var timeoutSelected = false + if let timeout = self.currentTimeout { + if timeout == viewOnceTimeout { + timeoutValue = "1" + } else { + timeoutValue = "\(timeout)" + } + timeoutSelected = true + } else { + timeoutValue = "1" + } + + var maxInputPanelHeight = maxHeight + if keyboardHeight.isZero { + 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), + component: AnyComponent( + MessageInputPanelComponent( + externalState: self.inputPanelExternalState, + context: self.context, + theme: theme, + strings: presentationData.strings, + style: .media, + placeholder: .plain(presentationData.strings.MediaPicker_AddCaption), + maxLength: 1024, + queryTypes: [.mention], + alwaysDarkWhenHasText: false, + resetInputContents: resetInputContents, + nextInputMode: { _ in + return .emoji + }, + areVoiceMessagesAvailable: false, + presentController: self.present, + presentInGlobalOverlay: self.presentInGlobalOverlay, + sendMessageAction: { [weak self] in + if let self { + self.sendPressed?(self.caption()) + self.dismissInput() + } + }, + sendMessageOptionsAction: nil, + sendStickerAction: { _ in }, + setMediaRecordingActive: nil, + lockMediaRecording: nil, + stopAndPreviewMediaRecording: nil, + discardMediaRecordingPreview: nil, + attachmentAction: nil, + myReaction: nil, + likeAction: nil, + likeOptionsAction: nil, + inputModeAction: nil, + timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView, gesture in + if let self { + self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) + } + } : nil, + forwardAction: nil, + moreAction: nil, + presentVoiceMessagesUnavailableTooltip: nil, + presentTextLengthLimitTooltip: nil, + presentTextFormattingTooltip: nil, + paste: { _ in }, + audioRecorder: nil, + videoRecordingStatus: nil, + isRecordingLocked: false, + recordedAudioPreview: nil, + hasRecordedVideoPreview: false, + wasRecordingDismissed: false, + timeoutValue: timeoutValue, + timeoutSelected: timeoutSelected, + displayGradient: false, + bottomInset: 0.0, + isFormattingLocked: false, + hideKeyboard: false, + forceIsEditing: false, + disabledPlaceholder: nil, + isChannel: false, + storyItem: nil, + chatLocation: self.chatLocation + ) + ), + environment: {}, + containerSize: CGSize(width: width, height: maxInputPanelHeight) + ) + if let view = self.inputPanel.view { + if view.superview == nil { + self.view.addSubview(view) + } + let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize) + transition.updateFrame(view: view, frame: inputPanelFrame) + } + + if self.currentIsEditing != self.inputPanelExternalState.isEditing { + self.currentIsEditing = self.inputPanelExternalState.isEditing + self.focusUpdated?(self.currentIsEditing) + } + + if self.currentHeight != inputPanelSize.height { + self.currentHeight = inputPanelSize.height + self.heightUpdated?(transition.isAnimated) + } + + return inputPanelSize.height - 8.0 + } + + private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) { + self.hapticFeedback.impact(.light) + + var items: [ContextMenuItem] = [] + + let updateTimeout: (Int32?) -> Void = { [weak self] timeout in + if let self { + self.currentTimeout = timeout + self.timerUpdated?(timeout as? NSNumber) + self.update(transition: .immediate) + self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout) + } + } + + let currentValue = self.currentTimeout + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) + 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: 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) + + updateTimeout(viewOnceTimeout) + }))) + + let values: [Int32] = [3, 10, 30] + + 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) + }))) + } + + 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) + + updateTimeout(nil) + }))) + + let contextController = ContextController(account: self.context.account, 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 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 = isVideo ? presentationData.strings.MediaPicker_Timer_Video_ViewOnceTooltip : presentationData.strings.MediaPicker_Timer_Photo_ViewOnceTooltip + iconName = "anim_autoremove_on" + } else if let timeout { + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_TimerTooltip("\(timeout)").string : presentationData.strings.MediaPicker_Timer_Photo_TimerTooltip("\(timeout)").string + iconName = "anim_autoremove_on" + } else { + text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip + iconName = "anim_autoremove_off" + } + + let tooltipController = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .plain(text: text), + balancedTextLayout: false, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: .animation(name: iconName, delay: 0.1, tintColor: nil), + location: .point(location, .bottom), + displayDuration: .default, + inset: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + self.present(tooltipController) + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) { + return panelResult + } + return result + } +} + +private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + var keepInPlace: Bool { + return true + } + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index a030a7852b..33a08101c0 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -407,7 +407,6 @@ final class MediaEditorScreenComponent: Component { guard let _ = self.inputPanel.view as? MessageInputPanelComponent.View else { return } -// if view.canDeactivateInput() { self.currentInputMode = .text if hasFirstResponder(self) { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { @@ -421,12 +420,6 @@ final class MediaEditorScreenComponent: Component { } else { self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) } -// } else { -// if let controller = self.environment?.controller() as? MediaEditorScreen { -// controller.presentCaptionLimitPremiumSuggestion(isPremium: self.sta) -// } -// view.animateError() -// } } private var animatingButtons = false @@ -498,7 +491,7 @@ final class MediaEditorScreenComponent: Component { func animateOut(to source: TransitionAnimationSource) { self.isDismissed = true - + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { transition.setAlpha(view: view, alpha: 0.0) @@ -965,7 +958,6 @@ final class MediaEditorScreenComponent: Component { } var timeoutValue: String - let timeoutSelected: Bool switch component.privacy.timeout { case 21600: timeoutValue = "6" @@ -978,7 +970,6 @@ final class MediaEditorScreenComponent: Component { default: timeoutValue = "24" } - timeoutSelected = false var inputPanelAvailableWidth = previewSize.width var inputPanelAvailableHeight = 103.0 @@ -1100,10 +1091,11 @@ final class MediaEditorScreenComponent: Component { theme: environment.theme, strings: environment.strings, style: .editor, - placeholder: environment.strings.Story_Editor_InputPlaceholderAddCaption, + placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), maxLength: Int(component.context.userLimits.maxStoryCaptionLength), queryTypes: [.mention], alwaysDarkWhenHasText: false, + resetInputContents: nil, nextInputMode: { _ in return nextInputMode }, areVoiceMessagesAvailable: false, presentController: { [weak self] c in @@ -1151,7 +1143,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 } @@ -1164,7 +1156,7 @@ final class MediaEditorScreenComponent: Component { } else { hasPremium = false } - controller?.presentTimeoutSetup(sourceView: view, hasPremium: hasPremium) + controller?.presentTimeoutSetup(sourceView: view, gesture: gesture, hasPremium: hasPremium) }) }, forwardAction: nil, @@ -1217,19 +1209,31 @@ final class MediaEditorScreenComponent: Component { hasRecordedVideoPreview: false, wasRecordingDismissed: false, timeoutValue: timeoutValue, - timeoutSelected: timeoutSelected, + timeoutSelected: false, displayGradient: false, bottomInset: 0.0, isFormattingLocked: !state.isPremium, hideKeyboard: self.currentInputMode == .emoji, forceIsEditing: self.currentInputMode == .emoji, disabledPlaceholder: nil, - storyId: nil + isChannel: false, + storyItem: nil, + chatLocation: nil )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) ) + if self.inputPanelExternalState.isEditing { + if let controller = self.environment?.controller() as? MediaEditorScreen { + if controller.node.entitiesView.hasSelection { + Queue.mainQueue().justDispatch { + controller.node.entitiesView.selectEntity(nil) + } + } + } + } + if self.inputPanelExternalState.isEditing { if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) @@ -1298,7 +1302,6 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } - let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) let saveContentComponent: AnyComponentWithIdentity @@ -2423,6 +2426,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.statusBar.statusBarStyle = .Ignore self.isUserInteractionEnabled = false + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } + let previousDimAlpha = self.backgroundDimView.alpha self.backgroundDimView.alpha = 0.0 self.backgroundDimView.layer.animateAlpha(from: previousDimAlpha, to: 0.0, duration: 0.15) @@ -2609,7 +2616,14 @@ 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 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 @@ -2788,7 +2802,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 { @@ -2897,8 +2919,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - private var drawingScreen: DrawingScreen? - private var stickerScreen: StickerPickerScreen? + fileprivate var drawingScreen: DrawingScreen? + fileprivate var stickerScreen: StickerPickerScreen? private var defaultToEmoji = false private var previousDrawingData: Data? @@ -3000,36 +3022,36 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case .sticker: self.mediaEditor?.stop() let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji, hasGifs: true) - controller.completion = { [weak self] content in - if let self { - if let content { - let stickerEntity = DrawingStickerEntity(content: content) - let scale: CGFloat - if case .image = content { - scale = 2.5 - } else if case .video = content { - scale = 2.5 - } else { - scale = 1.33 - } - self.interaction?.insertEntity(stickerEntity, scale: scale) - - self.hasAnyChanges = true - self.controller?.isSavingAvailable = true - self.controller?.requestLayout(transition: .immediate) - - if case let .file(file) = content { - if file.isCustomEmoji { - self.defaultToEmoji = true - } else { - self.defaultToEmoji = false - } - } - } - self.stickerScreen = nil - self.mediaEditor?.play() - } - } +// controller.completion = { [weak self] content in +// if let self { +// if let content { +// if case let .file(file, _) = content { +// if file.isCustomEmoji { +// self.defaultToEmoji = true +// } else { +// self.defaultToEmoji = false +// } +// } +// +// let stickerEntity = DrawingStickerEntity(content: content) +// let scale: CGFloat +// if case .image = content { +// scale = 2.5 +// } else if case .video = content { +// scale = 2.5 +// } else { +// scale = 1.33 +// } +// self.interaction?.insertEntity(stickerEntity, scale: scale) +// +// self.hasAnyChanges = true +// self.controller?.isSavingAvailable = true +// self.controller?.requestLayout(transition: .immediate) +// } +// self.stickerScreen = nil +// self.mediaEditor?.play() +// } +// } controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in if let self, let controller { let transitionFactor = controller.modalStyleOverlayTransitionFactor @@ -3053,6 +3075,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) return case .text: + self.mediaEditor?.stop() self.insertTextEntity() self.hasAnyChanges = true @@ -3542,6 +3565,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) } ) + controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in + if let self, let controller { + let transitionFactor = controller.modalStyleOverlayTransitionFactor + self.node.updateModalTransitionFactor(transitionFactor, transition: transition) + } + } controller.dismissed = { self.node.mediaEditor?.play() } @@ -3593,6 +3622,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate editCategory: { _, _, _ in }, editBlockedPeers: { _, _, _ in } ) + controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in + if let self, let controller { + let transitionFactor = controller.modalStyleOverlayTransitionFactor + self.node.updateModalTransitionFactor(transitionFactor, transition: transition) + } + } controller.dismissed = { self.node.mediaEditor?.play() } @@ -3600,7 +3635,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] = [] @@ -3615,7 +3650,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))) @@ -3651,7 +3685,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) @@ -3673,7 +3707,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }))) - let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } @@ -3694,7 +3728,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate ) self.present(controller, in: .current) } - + fileprivate func presentCaptionLimitPremiumSuggestion(isPremium: Bool) { self.dismissAllTooltips() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index 7e7905ae7b..c1345d6bec 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -1005,12 +1005,7 @@ public final class MediaToolsScreen: ViewController { let isFirstTime = self.validLayout == nil self.validLayout = layout - let isTablet: Bool - if case .regular = layout.metrics.widthClass { - isTablet = true - } else { - isTablet = false - } + let isTablet = layout.metrics.isTablet let previewSize: CGSize let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 5324035b2b..1cd86d4e2d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -250,10 +250,11 @@ final class StoryPreviewComponent: Component { theme: presentationData.theme, strings: presentationData.strings, style: .story, - placeholder: presentationData.strings.Story_InputPlaceholderReplyPrivately, + placeholder: .plain(presentationData.strings.Story_InputPlaceholderReplyPrivately), maxLength: nil, queryTypes: [], alwaysDarkWhenHasText: false, + resetInputContents: nil, nextInputMode: { _ in return .stickers }, areVoiceMessagesAvailable: false, presentController: { _ in }, @@ -291,7 +292,9 @@ final class StoryPreviewComponent: Component { hideKeyboard: false, forceIsEditing: false, disabledPlaceholder: nil, - storyId: nil + isChannel: false, + storyItem: nil, + chatLocation: nil )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 200.0) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index fd6453217f..7d18e062c0 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -33,6 +33,11 @@ swift_library( "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/StickerPeekUI", "//submodules/Components/ReactionButtonListComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/AnimatedCountLabelNode", + "//submodules/SearchPeerMembers", + "//submodules/ContextUI", + "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift index a0bf9324a3..0546d1cf9e 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift @@ -5,6 +5,7 @@ import TextFieldComponent import ChatContextQuery import AccountContext import TelegramUIPreferences +import SearchPeerMembers func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] { return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange) @@ -38,7 +39,7 @@ func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPr return result } -func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, availableTypes: [ChatPresentationInputQueryKind], currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { +func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, availableTypes: [ChatPresentationInputQueryKind], chatLocation: ChatLocation?, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] { let inputQueries = inputContextQueries(inputState).filter({ query in return availableTypes.contains(query.kind) }) @@ -48,7 +49,7 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo for query in inputQueries { let previousQuery = currentQueryStates[query.kind]?.0 if previousQuery != query { - let signal = updatedContextQueryResultStateForQuery(context: context, inputQuery: query, previousQuery: previousQuery) + let signal = updatedContextQueryResultStateForQuery(context: context, chatLocation: chatLocation, inputQuery: query, previousQuery: previousQuery) updates[query.kind] = .update(query, signal) } } @@ -69,7 +70,7 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo return updates } -private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { +private func updatedContextQueryResultStateForQuery(context: AccountContext, chatLocation: ChatLocation?, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> { switch inputQuery { case let .emoji(query): var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() @@ -149,7 +150,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp |> castError(ChatContextQueryError.self) return signal |> then(hashtags) - case let .mention(query, _): + case let .mention(query, types): var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete() if let previousQuery = previousQuery { switch previousQuery { @@ -163,34 +164,79 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp } let normalizedQuery = query.lowercased() - if normalizedQuery.isEmpty { - let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.peers.recentPeers() - |> map { recentPeers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - if case let .peers(peers) = recentPeers { - let peers = peers.filter { peer in - return peer.addressName != nil - }.compactMap { EnginePeer($0) } - return { _ in return .mentions(peers) } - } else { - return { _ in return .mentions([]) } - } - } - |> castError(ChatContextQueryError.self) - return signal |> then(peers) - } else { - let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery) - |> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let peers = peersAndPresences.filter { peer in - if let peer = peer.peer, case .user = peer, peer.addressName != nil { - return true - } else { + + if let chatLocation, let peerId = chatLocation.peerId { + let inlineBots: Signal<[(EnginePeer, Double)], NoError> = types.contains(.contextBots) ? context.engine.peers.recentlyUsedInlineBots() : .single([]) + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peerId, chatLocation: chatLocation, query: query, scope: .mention)) + |> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in + if rating < 0.14 { return false } - }.compactMap { $0.peer } - return { _ in return .mentions(peers) } + if peer.indexName.matchesByTokens(normalizedQuery) { + return true + } + if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return true + } + return false + }.map { $0.0 } + + let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id }) + + let filteredPeers = peers.filter { peer in + if inlineBotPeerIds.contains(peer.id) { + return false + } + if !types.contains(.accountPeer) && peer.id == context.account.peerId { + return false + } + return true + } + var sortedPeers = filteredInlineBots + sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true)) + return result == .orderedAscending + })) + sortedPeers = sortedPeers.filter { peer in + return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty + } + return { _ in return .mentions(sortedPeers) } } |> castError(ChatContextQueryError.self) - return signal |> then(peers) + + return signal |> then(participants) + } else { + if normalizedQuery.isEmpty { + let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.peers.recentPeers() + |> map { recentPeers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + if case let .peers(peers) = recentPeers { + let peers = peers.filter { peer in + return peer.addressName != nil + }.compactMap { EnginePeer($0) } + return { _ in return .mentions(peers) } + } else { + return { _ in return .mentions([]) } + } + } + |> castError(ChatContextQueryError.self) + return signal |> then(peers) + } else { + let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery) + |> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let peers = peersAndPresences.filter { peer in + if let peer = peer.peer, case .user = peer, peer.addressName != nil { + return true + } else { + return false + } + }.compactMap { $0.peer } + return { _ in return .mentions(peers) } + } + |> castError(ChatContextQueryError.self) + return signal |> then(peers) + } } case let .emojiSearch(query, languageCode, range): let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 2c36d7c134..e561ebbf50 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -15,6 +15,11 @@ import ChatContextQuery import TextFormat import EmojiSuggestionsComponent import AudioToolbox +import AnimatedTextComponent +import AnimatedCountLabelNode +import ContextReferenceButtonComponent + +private let timeoutButtonTag = GenericComponentViewTag() public final class MessageInputPanelComponent: Component { public struct ContextQueryTypes: OptionSet { @@ -36,6 +41,7 @@ public final class MessageInputPanelComponent: Component { public enum Style { case story case editor + case media } public enum InputMode: Hashable { @@ -56,6 +62,26 @@ public final class MessageInputPanelComponent: Component { } } + public enum Placeholder: Equatable { + public enum CounterItemContent: Equatable { + case text(String) + case number(Int, minDigits: Int) + } + + public struct CounterItem: Equatable { + public var id: Int + public var content: CounterItemContent + + public init(id: Int, content: CounterItemContent) { + self.id = id + self.content = content + } + } + + case plain(String) + case counter([CounterItem]) + } + public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false @@ -75,10 +101,11 @@ public final class MessageInputPanelComponent: Component { public let theme: PresentationTheme public let strings: PresentationStrings public let style: Style - public let placeholder: String + public let placeholder: Placeholder public let maxLength: Int? public let queryTypes: ContextQueryTypes public let alwaysDarkWhenHasText: Bool + public let resetInputContents: SendMessageInput? public let nextInputMode: (Bool) -> InputMode? public let areVoiceMessagesAvailable: Bool public let presentController: (ViewController) -> Void @@ -95,7 +122,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)? @@ -116,7 +143,9 @@ public final class MessageInputPanelComponent: Component { public let hideKeyboard: Bool public let forceIsEditing: Bool public let disabledPlaceholder: String? - public let storyId: Int32? + public let isChannel: Bool + public let storyItem: EngineStoryItem? + public let chatLocation: ChatLocation? public init( externalState: ExternalState, @@ -124,10 +153,11 @@ public final class MessageInputPanelComponent: Component { theme: PresentationTheme, strings: PresentationStrings, style: Style, - placeholder: String, + placeholder: Placeholder, maxLength: Int?, queryTypes: ContextQueryTypes, alwaysDarkWhenHasText: Bool, + resetInputContents: SendMessageInput?, nextInputMode: @escaping (Bool) -> InputMode?, areVoiceMessagesAvailable: Bool, presentController: @escaping (ViewController) -> Void, @@ -144,7 +174,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)?, @@ -165,7 +195,9 @@ public final class MessageInputPanelComponent: Component { hideKeyboard: Bool, forceIsEditing: Bool, disabledPlaceholder: String?, - storyId: Int32? + isChannel: Bool, + storyItem: EngineStoryItem?, + chatLocation: ChatLocation? ) { self.externalState = externalState self.context = context @@ -177,6 +209,7 @@ public final class MessageInputPanelComponent: Component { self.maxLength = maxLength self.queryTypes = queryTypes self.alwaysDarkWhenHasText = alwaysDarkWhenHasText + self.resetInputContents = resetInputContents self.areVoiceMessagesAvailable = areVoiceMessagesAvailable self.presentController = presentController self.presentInGlobalOverlay = presentInGlobalOverlay @@ -213,7 +246,9 @@ public final class MessageInputPanelComponent: Component { self.hideKeyboard = hideKeyboard self.forceIsEditing = forceIsEditing self.disabledPlaceholder = disabledPlaceholder - self.storyId = storyId + self.isChannel = isChannel + self.storyItem = storyItem + self.chatLocation = chatLocation } public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool { @@ -244,6 +279,9 @@ public final class MessageInputPanelComponent: Component { if lhs.alwaysDarkWhenHasText != rhs.alwaysDarkWhenHasText { return false } + if lhs.resetInputContents != rhs.resetInputContents { + return false + } if lhs.areVoiceMessagesAvailable != rhs.areVoiceMessagesAvailable { return false } @@ -307,13 +345,19 @@ public final class MessageInputPanelComponent: Component { if (lhs.likeOptionsAction == nil) != (rhs.likeOptionsAction == nil) { return false } - if lhs.storyId != rhs.storyId { + if lhs.isChannel != rhs.isChannel { + return false + } + if lhs.storyItem != rhs.storyItem { + return false + } + if lhs.chatLocation != rhs.chatLocation { return false } return true } - public enum SendMessageInput { + public enum SendMessageInput: Equatable { case text(NSAttributedString) } @@ -329,6 +373,7 @@ public final class MessageInputPanelComponent: Component { private let counter = ComponentView() private var disabledPlaceholder: ComponentView? + private var textClippingView = UIView() private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() @@ -360,11 +405,17 @@ public final class MessageInputPanelComponent: Component { private var viewForOverlayContent: ViewForOverlayContent? private var currentEmojiSuggestionView: ComponentHostView? + private var viewsIconView: UIImageView? + private var viewStatsCountText: AnimatedCountLabelView? + private var reactionStatsCountText: AnimatedCountLabelView? + private let hapticFeedback = HapticFeedback() private var component: MessageInputPanelComponent? private weak var state: EmptyComponentState? + private var pendingSetMessageInput: SendMessageInput? + public var likeButtonView: UIView? { return self.likeButton.view } @@ -380,6 +431,7 @@ public final class MessageInputPanelComponent: Component { let blurEffect = UIBlurEffect(style: style) let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect) let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect) + vibrancyEffectView.alpha = 0.0 self.vibrancyEffectView = vibrancyEffectView self.mediaRecordingVibrancyContainer = UIView() @@ -388,12 +440,15 @@ public final class MessageInputPanelComponent: Component { self.gradientView = UIImageView() self.bottomGradientView = UIView() + self.textClippingView.clipsToBounds = true + super.init(frame: frame) self.addSubview(self.bottomGradientView) self.addSubview(self.gradientView) self.fieldBackgroundView.addSubview(self.vibrancyEffectView) self.addSubview(self.fieldBackgroundView) + self.addSubview(self.textClippingView) self.viewForOverlayContent = ViewForOverlayContent( ignoreHit: { [weak self] view, point in @@ -422,6 +477,14 @@ public final class MessageInputPanelComponent: Component { fatalError("init(coder:) has not been implemented") } + public func hasFirstResponder() -> Bool { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.hasFirstResponder() + } else { + return false + } + } + public func getSendMessageInput() -> SendMessageInput { guard let textFieldView = self.textField.view as? TextFieldComponent.View else { return .text(NSAttributedString()) @@ -430,6 +493,17 @@ public final class MessageInputPanelComponent: Component { return .text(textFieldView.getAttributedText()) } + public func setSendMessageInput(value: SendMessageInput, updateState: Bool) { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + switch value { + case let .text(text): + textFieldView.setAttributedText(text, updateState: updateState) + } + } else { + self.pendingSetMessageInput = value + } + } + public func getAttachmentButtonView() -> UIView? { guard let attachmentButtonView = self.attachmentButton.view else { return nil @@ -437,9 +511,9 @@ public final class MessageInputPanelComponent: Component { return attachmentButtonView } - public func clearSendMessageInput() { + public func clearSendMessageInput(updateState: Bool) { if let textFieldView = self.textField.view as? TextFieldComponent.View { - textFieldView.setAttributedText(NSAttributedString()) + textFieldView.setAttributedText(NSAttributedString(), updateState: updateState) } } @@ -498,7 +572,7 @@ public final class MessageInputPanelComponent: Component { if component.queryTypes.contains(.emoji) { availableTypes.append(.emoji) } - let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, currentQueryStates: &self.contextQueryStates) + let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, chatLocation: component.chatLocation, currentQueryStates: &self.contextQueryStates) for (kind, update) in contextQueryUpdates { switch update { @@ -560,6 +634,8 @@ public final class MessageInputPanelComponent: Component { } func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousPlaceholder = self.component?.placeholder + var insets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0) if let _ = component.attachmentAction { @@ -569,9 +645,19 @@ public final class MessageInputPanelComponent: Component { insets.right = 41.0 } - let mediaInsets = UIEdgeInsets(top: insets.top, left: 9.0, bottom: insets.bottom, right: 41.0) + var textFieldSideInset = 9.0 + if case .media = component.style { + textFieldSideInset = 8.0 + } + + let mediaInsets = UIEdgeInsets(top: insets.top, left: textFieldSideInset, bottom: insets.bottom, right: 41.0) let baseFieldHeight: CGFloat = 40.0 + + var transition = transition + if transition.animation.isImmediate, let previousComponent = self.component, previousComponent.storyItem?.id == component.storyItem?.id, component.isChannel { + transition = transition.withAnimation(.curve(duration: 0.3, curve: .spring)) + } self.component = component self.state = state @@ -626,6 +712,13 @@ public final class MessageInputPanelComponent: Component { textColor: UIColor(rgb: 0xffffff), insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0), hideKeyboard: component.hideKeyboard, + resetText: component.resetInputContents.flatMap { resetInputContents in + switch resetInputContents { + case let .text(value): + return value + } + }, + isOneLineWhenUnfocused: component.style == .media, formatMenuAvailability: component.isFormattingLocked ? .locked : .available, lockedFormatAction: { component.presentTextFormattingTooltip?() @@ -642,23 +735,39 @@ public final class MessageInputPanelComponent: Component { ) let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing + var placeholderItems: [AnimatedTextComponent.Item] = [] + switch component.placeholder { + case let .plain(string): + placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .text(string))) + case let .counter(items): + for item in items { + switch item.content { + case let .text(string): + placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .text(string))) + case let .number(value, minDigits): + placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .number(value, minDigits: minDigits))) + } + } + } + + let placeholderTransition: Transition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? Transition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate let placeholderSize = self.placeholder.update( - transition: .immediate, - component: AnyComponent(Text( - text: component.placeholder, + transition: placeholderTransition, + component: AnyComponent(AnimatedTextComponent( font: Font.regular(17.0), - color: UIColor(rgb: 0xffffff, alpha: 0.3) + color: UIColor(rgb: 0xffffff, alpha: 0.3), + items: placeholderItems )), environment: {}, containerSize: availableTextFieldSize ) let _ = self.vibrancyPlaceholder.update( - transition: .immediate, - component: AnyComponent(Text( - text: component.placeholder, + transition: placeholderTransition, + component: AnyComponent(AnimatedTextComponent( font: Font.regular(17.0), - color: .white + color: .white, + items: placeholderItems )), environment: {}, containerSize: availableTextFieldSize @@ -672,20 +781,33 @@ public final class MessageInputPanelComponent: Component { var fieldBackgroundFrame: CGRect if hasMediaRecording { fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: textFieldSize.height)) - } else if isEditing { + } else if isEditing || component.style == .editor || component.style == .media { fieldBackgroundFrame = fieldFrame } else { - fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right, height: textFieldSize.height)) - if component.likeAction != nil && component.forwardAction != nil { - fieldBackgroundFrame.size.width -= 49.0 + if component.forwardAction != nil && component.likeAction != nil { + fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right - 49.0, height: textFieldSize.height)) + } else if component.forwardAction != nil { + fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right, height: textFieldSize.height)) + } else { + fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - 50.0, height: textFieldSize.height)) } } transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) + self.vibrancyEffectView.isHidden = component.style == .media + if isEditing { + self.vibrancyEffectView.alpha = 1.0 + } transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame) self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition) + var textClippingFrame = fieldBackgroundFrame + if component.style == .media, !isEditing { + textClippingFrame.size.height -= 10.0 + } + transition.setFrame(view: self.textClippingView, frame: textClippingFrame) + let gradientFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX - fieldFrame.minX, y: -topGradientHeight), size: CGSize(width: availableSize.width - (fieldBackgroundFrame.minX - fieldFrame.minX), height: topGradientHeight + fieldBackgroundFrame.maxY + insets.bottom)) transition.setFrame(view: self.gradientView, frame: gradientFrame) transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset))) @@ -715,32 +837,120 @@ public final class MessageInputPanelComponent: Component { transition.setPosition(view: placeholderView, position: placeholderFrame.origin) placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) - transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) - transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) + transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0) + transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0) } - transition.setAlpha(view: self.fieldBackgroundView, alpha: component.disabledPlaceholder != nil ? 0.0 : 1.0) + transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0) let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom) - if let textFieldView = self.textField.view { + var rightButtonsOffsetX: CGFloat = 0.0 + if component.isChannel, let storyItem = component.storyItem { + var viewsTransition = transition + + let viewsIconView: UIImageView + if let current = self.viewsIconView { + viewsIconView = current + } else { + viewsTransition = viewsTransition.withAnimation(.none) + viewsIconView = UIImageView(image: UIImage(bundleImageName: "Stories/EmbeddedViewIcon")) + self.viewsIconView = viewsIconView + self.addSubview(viewsIconView) + } + + let viewStatsCountText: AnimatedCountLabelView + if let current = self.viewStatsCountText { + viewStatsCountText = current + } else { + viewStatsCountText = AnimatedCountLabelView(frame: CGRect()) + self.viewStatsCountText = viewStatsCountText + self.addSubview(viewStatsCountText) + } + + let reactionStatsCountText: AnimatedCountLabelView + if let current = self.reactionStatsCountText { + reactionStatsCountText = current + } else { + reactionStatsCountText = AnimatedCountLabelView(frame: CGRect()) + self.reactionStatsCountText = reactionStatsCountText + self.addSubview(reactionStatsCountText) + } + + var viewCount = storyItem.views?.seenCount ?? 0 + if viewCount == 0 { + viewCount = 1 + } + var reactionCount = storyItem.views?.reactedCount ?? 0 + if reactionCount == 0, storyItem.myReaction != nil { + reactionCount += 1 + } + + var regularSegments: [AnimatedCountLabelView.Segment] = [] + regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white))) + + var reactionSegments: [AnimatedCountLabelView.Segment] = [] + reactionSegments.append(.number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white))) + + let viewStatsTextLayout = viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: viewsTransition.containedViewLayoutTransition) + let reactionStatsTextLayout = reactionStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: reactionSegments, transition: viewsTransition.containedViewLayoutTransition) + + var contentX: CGFloat = 16.0 + + if let image = viewsIconView.image { + let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - image.size.height) * 0.5)), size: image.size) + viewsTransition.setPosition(view: viewsIconView, position: viewsIconFrame.center) + viewsTransition.setBounds(view: viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size)) + + contentX += image.size.width + 5.0 + } + + transition.setFrame(view: viewStatsCountText, frame: CGRect(origin: CGPoint(x: contentX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - viewStatsTextLayout.size.height) * 0.5)), size: viewStatsTextLayout.size)) + + transition.setFrame(view: reactionStatsCountText, frame: CGRect(origin: CGPoint(x: availableSize.width - 11.0 - reactionStatsTextLayout.size.width, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - reactionStatsTextLayout.size.height) * 0.5)), size: reactionStatsTextLayout.size)) + + rightButtonsOffsetX -= reactionStatsTextLayout.size.width + 4.0 + } else { + if let viewsIconView = self.viewsIconView { + self.viewsIconView = nil + viewsIconView.removeFromSuperview() + } + if let viewStatsCountText = self.viewStatsCountText { + self.viewStatsCountText = nil + viewStatsCountText.removeFromSuperview() + } + if let reactionStatsCountText = self.reactionStatsCountText { + self.reactionStatsCountText = nil + reactionStatsCountText.removeFromSuperview() + } + } + + if let textFieldView = self.textField.view as? TextFieldComponent.View { if textFieldView.superview == nil { - self.addSubview(textFieldView) + self.textClippingView.addSubview(textFieldView) if let viewForOverlayContent = self.viewForOverlayContent { self.addSubview(viewForOverlayContent) } + + if let pendingSetMessageInput = self.pendingSetMessageInput { + self.pendingSetMessageInput = nil + switch pendingSetMessageInput { + case let .text(text): + textFieldView.setAttributedText(text, updateState: false) + } + } } - let textFieldFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize) + let textFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textFieldSize) transition.setFrame(view: textFieldView, frame: textFieldFrame) - transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0) + transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0) if let viewForOverlayContent = self.viewForOverlayContent { transition.setFrame(view: viewForOverlayContent, frame: textFieldFrame) } } - if let disabledPlaceholderText = component.disabledPlaceholder { + if let disabledPlaceholderText = component.disabledPlaceholder, !component.isChannel { let disabledPlaceholder: ComponentView var disabledPlaceholderTransition = transition if let current = self.disabledPlaceholder { @@ -809,7 +1019,7 @@ public final class MessageInputPanelComponent: Component { transition: transition, component: AnyComponent(MessageInputActionButtonComponent( mode: attachmentButtonMode, - storyId: component.storyId, + storyId: component.storyItem?.id, action: { [weak self] mode, action, sendAction in guard let self, let component = self.component, case .up = action else { return @@ -937,6 +1147,8 @@ public final class MessageInputPanelComponent: Component { let inputActionButtonMode: MessageInputActionButtonComponent.Mode if case .editor = component.style { inputActionButtonMode = isEditing ? .apply : .none + } else if case .media = component.style { + inputActionButtonMode = isEditing ? .apply : .none } else { if hasMediaEditing { inputActionButtonMode = .send @@ -958,7 +1170,7 @@ public final class MessageInputPanelComponent: Component { transition: transition, component: AnyComponent(MessageInputActionButtonComponent( mode: inputActionButtonMode, - storyId: component.storyId, + storyId: component.storyItem?.id, action: { [weak self] mode, action, sendAction in guard let self, let component = self.component else { return @@ -1006,7 +1218,7 @@ public final class MessageInputPanelComponent: Component { break } }, - longPressAction: component.sendMessageOptionsAction, + longPressAction: inputActionButtonMode == .send ? component.sendMessageOptionsAction : nil, switchMediaInputMode: { [weak self] in guard let self else { return @@ -1067,14 +1279,24 @@ public final class MessageInputPanelComponent: Component { } var inputActionButtonOriginX: CGFloat - if component.setMediaRecordingActive != nil || isEditing { - inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5) + if rightButtonsOffsetX != 0.0 { + inputActionButtonOriginX = availableSize.width - 3.0 + rightButtonsOffsetX + if displayLikeAction { + inputActionButtonOriginX -= 39.0 + } + if component.forwardAction != nil { + inputActionButtonOriginX -= 46.0 + } } else { - inputActionButtonOriginX = size.width - } - - if hasLikeAction { - inputActionButtonOriginX += 3.0 + if component.setMediaRecordingActive != nil || isEditing { + inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5) + } else { + inputActionButtonOriginX = size.width + } + + if hasLikeAction { + inputActionButtonOriginX += 3.0 + } } if let inputActionButtonView = self.inputActionButton.view { @@ -1086,8 +1308,14 @@ public final class MessageInputPanelComponent: Component { transition.setBounds(view: inputActionButtonView, bounds: CGRect(origin: CGPoint(), size: inputActionButtonFrame.size)) transition.setAlpha(view: inputActionButtonView, alpha: likeActionReplacesInputAction ? 0.0 : 1.0) - if hasLikeAction { - inputActionButtonOriginX += 41.0 + if rightButtonsOffsetX != 0.0 { + if hasLikeAction { + inputActionButtonOriginX += 46.0 + } + } else { + if hasLikeAction { + inputActionButtonOriginX += 41.0 + } } } @@ -1095,7 +1323,7 @@ public final class MessageInputPanelComponent: Component { transition: transition, component: AnyComponent(MessageInputActionButtonComponent( mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId), - storyId: component.storyId, + storyId: component.storyItem?.id, action: { [weak self] _, action, _ in guard let self, let component = self.component else { return @@ -1129,7 +1357,10 @@ public final class MessageInputPanelComponent: Component { if likeButtonView.superview == nil { self.addSubview(likeButtonView) } - let likeButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - likeButtonSize.height) * 0.5)), size: likeButtonSize) + var likeButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - likeButtonSize.height) * 0.5)), size: likeButtonSize) + if component.forwardAction == nil && rightButtonsOffsetX == 0.0 { + likeButtonFrame.origin.x += 3.0 + } transition.setPosition(view: likeButtonView, position: likeButtonFrame.center) transition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size)) transition.setAlpha(view: likeButtonView, alpha: displayLikeAction ? 1.0 : 0.0) @@ -1239,45 +1470,25 @@ 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) -> 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 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) - })?.withRenderingMode(.alwaysTemplate) - } - - let icon = generateIcon(value: timeoutValue) let timeoutButtonSize = self.timeoutButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Image(image: icon, tintColor: component.timeoutSelected ? UIColor(rgb: 0xf8d74a) : UIColor(white: 1.0, alpha: 1.0), 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) ) @@ -1296,7 +1507,9 @@ public final class MessageInputPanelComponent: Component { } var fieldBackgroundIsDark = false - if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText { + if component.style == .media { + + } else if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText { fieldBackgroundIsDark = true } else if isEditing || component.style == .editor { fieldBackgroundIsDark = true @@ -1468,8 +1681,12 @@ public final class MessageInputPanelComponent: Component { self.updateContextQueries() - let panelLeftInset: CGFloat = max(insets.left, 7.0) - let panelRightInset: CGFloat = max(insets.right, 41.0) + var panelLeftInset: CGFloat = max(insets.left, 7.0) + var panelRightInset: CGFloat = max(insets.right, 41.0) + if case .media = component.style { + panelLeftInset = 0.0 + panelRightInset = 0.0 + } var contextResults: ContextResultPanelComponent.Results? if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty { @@ -1601,7 +1818,13 @@ public final class MessageInputPanelComponent: Component { containerSize: CGSize(width: availableSize.width - panelLeftInset - panelRightInset, height: availablePanelHeight) ) - let panelFrame = CGRect(origin: CGPoint(x: insets.left, y: -panelSize.height + 14.0), size: CGSize(width: panelSize.width, height: panelSize.height + 19.0)) + var panelOriginY = -panelSize.height + 14.0 + var panelHeight = panelSize.height + 19.0 + if case .media = component.style { + panelOriginY -= 6.0 + panelHeight = panelSize.height + } + let panelFrame = CGRect(origin: CGPoint(x: panelLeftInset, y: panelOriginY), size: CGSize(width: panelSize.width, height: panelHeight)) if let panelView = panel.view as? ContextResultPanelComponent.View { if panelView.superview == nil { self.insertSubview(panelView, at: 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/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift index d572b6f50d..9acf92ccfa 100644 --- a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/Sources/NewSessionInfoScreen.swift @@ -105,7 +105,7 @@ private final class NewSessionInfoSheetContentComponent: Component { if self.remainingTimer > 0 { buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent( AnimatedTextComponent(font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), color: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), items: [ - AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .number(self.remainingTimer)) + AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .number(self.remainingTimer, minDigits: 0)) ]) ))) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 547f2f5083..424d4b3723 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2296,7 +2296,7 @@ public final class StoryItemSetContainerComponent: Component { if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { Queue.mainQueue().justDispatch { - inputPanelView.clearSendMessageInput() + inputPanelView.clearSendMessageInput(updateState: true) } } @@ -2457,10 +2457,11 @@ public final class StoryItemSetContainerComponent: Component { theme: component.theme, strings: component.strings, style: .story, - placeholder: inputPlaceholder, + placeholder: .plain(inputPlaceholder), maxLength: 4096, queryTypes: [.mention, .emoji], alwaysDarkWhenHasText: component.metrics.widthClass == .regular, + resetInputContents: nil, nextInputMode: { [weak self] hasText in if case .media = self?.sendMessageContext.currentInputMode { return .text @@ -2652,7 +2653,9 @@ public final class StoryItemSetContainerComponent: Component { hideKeyboard: self.sendMessageContext.currentInputMode == .media, forceIsEditing: self.sendMessageContext.currentInputMode == .media, disabledPlaceholder: disabledPlaceholder, - storyId: component.slice.item.storyItem.id + isChannel: false, + storyItem: component.slice.item.storyItem, + chatLocation: nil )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 7172f87497..ef7ee915ba 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -601,7 +601,7 @@ final class StoryItemSetContainerSendMessage { } } }) - inputPanelView.clearSendMessageInput() + inputPanelView.clearSendMessageInput(updateState: true) self.currentInputMode = .text if hasFirstResponder(view) { @@ -1259,7 +1259,7 @@ final class StoryItemSetContainerSendMessage { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - inputPanelView.clearSendMessageInput() + inputPanelView.clearSendMessageInput(updateState: true) } enum AttachMenuSubject { diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index dc81a1c4fb..beb222bc18 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -90,6 +90,8 @@ public final class TextFieldComponent: Component { public let textColor: UIColor public let insets: UIEdgeInsets public let hideKeyboard: Bool + public let resetText: NSAttributedString? + public let isOneLineWhenUnfocused: Bool public let formatMenuAvailability: FormatMenuAvailability public let lockedFormatAction: () -> Void public let present: (ViewController) -> Void @@ -103,6 +105,8 @@ public final class TextFieldComponent: Component { textColor: UIColor, insets: UIEdgeInsets, hideKeyboard: Bool, + resetText: NSAttributedString?, + isOneLineWhenUnfocused: Bool, formatMenuAvailability: FormatMenuAvailability, lockedFormatAction: @escaping () -> Void, present: @escaping (ViewController) -> Void, @@ -115,6 +119,8 @@ public final class TextFieldComponent: Component { self.textColor = textColor self.insets = insets self.hideKeyboard = hideKeyboard + self.resetText = resetText + self.isOneLineWhenUnfocused = isOneLineWhenUnfocused self.formatMenuAvailability = formatMenuAvailability self.lockedFormatAction = lockedFormatAction self.present = present @@ -140,6 +146,12 @@ public final class TextFieldComponent: Component { if lhs.hideKeyboard != rhs.hideKeyboard { return false } + if lhs.resetText != rhs.resetText { + return false + } + if lhs.isOneLineWhenUnfocused != rhs.isOneLineWhenUnfocused { + return false + } if lhs.formatMenuAvailability != rhs.formatMenuAvailability { return false } @@ -189,6 +201,8 @@ public final class TextFieldComponent: Component { private var customEmojiContainerView: CustomEmojiContainerView? private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? + private let ellipsisView = ComponentView() + private var inputState: InputState { let selectionRange: Range = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length) return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange) @@ -229,6 +243,10 @@ public final class TextFieldComponent: Component { self.textContainer.widthTracksTextView = false self.textContainer.heightTracksTextView = false + if #available(iOS 13.0, *) { + self.textView.overrideUserInterfaceStyle = .dark + } + self.textView.typingAttributes = [ NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: UIColor.white @@ -258,6 +276,10 @@ public final class TextFieldComponent: Component { self.updateEntities() } + public func hasFirstResponder() -> Bool { + return self.textView.isFirstResponder + } + public func insertText(_ text: NSAttributedString) { self.updateInputState { state in return state.insertText(text) @@ -523,11 +545,13 @@ public final class TextFieldComponent: Component { return self.inputState.inputText } - public func setAttributedText(_ string: NSAttributedString) { + public func setAttributedText(_ string: NSAttributedString, updateState: Bool) { self.updateInputState { _ in return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length) } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged))) + if updateState { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged))) + } } public func activateInput() { @@ -774,6 +798,30 @@ public final class TextFieldComponent: Component { component.externalState.hasTrackingView = hasTrackingView } + func rightmostPositionOfFirstLine() -> CGPoint? { + let glyphRange = self.layoutManager.glyphRange(for: self.textContainer) + + if glyphRange.length == 0 { return nil } + + var lineRect = CGRect.zero + var glyphIndexForStringStart = glyphRange.location + var lineRange: NSRange = NSRange() + + repeat { + lineRect = self.layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndexForStringStart, effectiveRange: &lineRange) + if NSMaxRange(lineRange) > glyphRange.length { + lineRange.length = glyphRange.length - lineRange.location + } + glyphIndexForStringStart = NSMaxRange(lineRange) + } while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange) + + let padding = self.textView.textContainerInset.left + let rightmostX = lineRect.maxX + padding + let rightmostY = lineRect.minY + self.textView.textContainerInset.top + + return CGPoint(x: rightmostX, y: rightmostY) + } + func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state @@ -783,6 +831,10 @@ public final class TextFieldComponent: Component { self.updateInputState { _ in return TextFieldComponent.InputState(inputText: initialText) } + } else if let resetText = component.resetText { + self.updateInputState { _ in + return TextFieldComponent.InputState(inputText: resetText) + } } if self.emojiViewProvider == nil { @@ -807,7 +859,10 @@ public final class TextFieldComponent: Component { let wasEditing = component.externalState.isEditing let isEditing = self.textView.isFirstResponder - let refreshScrolling = self.textView.bounds.size != size + var refreshScrolling = self.textView.bounds.size != size + if component.isOneLineWhenUnfocused && !isEditing && isEditing != wasEditing { + refreshScrolling = true + } self.textView.frame = CGRect(origin: CGPoint(), size: size) self.textView.panGestureRecognizer.isEnabled = isEditing @@ -815,7 +870,7 @@ public final class TextFieldComponent: Component { if refreshScrolling { if isEditing { - if wasEditing { + if wasEditing || component.isOneLineWhenUnfocused { self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false) } } else { @@ -843,6 +898,43 @@ public final class TextFieldComponent: Component { } } + if component.isOneLineWhenUnfocused, let position = self.rightmostPositionOfFirstLine() { + let ellipsisSize = self.ellipsisView.update( + transition: transition, + component: AnyComponent( + Text( + text: "\u{2026}", + font: Font.regular(component.fontSize), + color: component.textColor + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.ellipsisView.view { + if view.superview == nil { + view.alpha = 0.0 + self.textView.addSubview(view) + } + let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 11.0, y: position.y), size: ellipsisSize) + transition.setFrame(view: view, frame: ellipsisFrame) + + let hasMoreThanOneLine = ellipsisFrame.maxY < self.textView.contentSize.height - 12.0 + + let ellipsisTransition: Transition + if isEditing { + ellipsisTransition = .easeInOut(duration: 0.2) + } else { + ellipsisTransition = .easeInOut(duration: 0.3) + } + ellipsisTransition.setAlpha(view: view, alpha: isEditing || !hasMoreThanOneLine ? 0.0 : 1.0) + } + } else { + if let view = self.ellipsisView.view { + view.removeFromSuperview() + } + } + self.updateEntities() return size diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json deleted file mode 100644 index ef12df2de0..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "SecretMediaIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "SecretMediaIcon@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@2x.png deleted file mode 100644 index 9c98438f62e625671cd18ccf6f7fd4660cc863f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 753 zcmVV9fy+;wGSbK`f%!7oQtTv1R11o7eaL%HsdNp?L8Zrz{FdoMVuxU1Aum|2E$k8Gc#Vv4Y6l6qeAqS0cDx%Z_ zGNK(&NWUSDnn6YmFX_&RtLl&u&4(=Q#7&5+rjQX0gDka1ob?SnA=D1C^b_K231qMk zURlV}+lZ^Xp#THmJqKBO3vrbX3h*zyQ?S`-ZMkLfIT zGGwUh>nlH$X0324ock1;pqTAJQ5=WJU4lADK{4owI}m&KqP^peNnsFPM%p_IAD|7E j8P4o#^uf2d6rtM(1ByE0>F%z900000NkvXXu0mjf+_h3$ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaIcon.imageset/SecretMediaIcon@3x.png deleted file mode 100644 index cbf683dca1b3deb433c993839c447a3b8e356a99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1138 zcmV-&1daQNP)7sVrkfLLV>e{B9w>uasK**vAGp*;DdtBe zy9&zT2aH4JIt0oEZ({&5)do<6yO62og*vb^GE+aO1ErXX%v1z*;Cb{wCi)cWgu{@D zo`pK01`C)B^dQtp?YdK(NmD6VvK-OJ@F>=j};N3_n{chhEw|>^t{&>)ldvIE-SDxD27^i3Q4yjc(8}V1R+^1js>goau8hC>I=qr0R{IAxFEys)wLlFcSljL|a21ylpI6-2HbFtN!=` z@??ovpKgSW;j{B0kFGPFXnxDVVQG(F_g##_znunI`ogqm8kAC3Vvyx=YA%iF2Qw8)q2D9c=u@B=7J*p{ zN}0Pb5>b^xmUe*I8%lvD++(rbdo7<3t74v<_;*ZnYbC-VL0M+ za?GEKc|FkA^-Y|BrO*Rj4hSqg0rM%8ke_ilM#Fa-LY7{K`2q5*21lSbl4=>q(x)&p zA&n`bQOh$<2hAfrAG3!8`*wS_h%q3$pYK%;k`yK?vO?kfqCE4uu?)p)2|xvUDiS z3Xp^E5W4xH7*=q*F#}9P=#GndblTVa({qkHp6@(0I1Vl?h1VcQE22|f3^{rR zi^Nzm-h^hz(Z$B1K6n*!)P#qz3&x^lJunu_;ADIbdGadyz^VzDmcX&fH~0~CPy(i5 z0%Ely>Y+SQj}_t6fl!_}5MJ5~4NyuoU@v%W9h5^UQ;v0zNDJc^C?$Tu!br49cmwKU zZ(t&V)DwrG0t&1GhoC2dHw9OtKG2kNHKv#pG#f5QDdbrxF2`(0T?4Qq9!9z2$onvM z!~o<)eXt@9$2E8r-=hF^|M+9Cp<+j%YKowGSZU=b0KaK;7_GcF+5i9m07*qoM6N<$ Ef-(aFumAu6 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/Contents.json new file mode 100644 index 0000000000..ba266bf499 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "miniplayonce.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/miniplayonce.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/miniplayonce.pdf new file mode 100644 index 0000000000..7ec1ebc79c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/miniplayonce.pdf @@ -0,0 +1,107 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.000000 1.669922 cm +0.000000 0.000000 0.000000 scn +4.000000 8.665078 m +4.367270 8.665078 4.665000 8.962809 4.665000 9.330078 c +4.665000 9.697348 4.367270 9.995078 4.000000 9.995078 c +4.000000 8.665078 l +h +8.665000 5.330078 m +8.665000 5.697348 8.367270 5.995078 8.000000 5.995078 c +7.632730 5.995078 7.335000 5.697348 7.335000 5.330078 c +8.665000 5.330078 l +h +4.000000 9.995078 m +1.423591 9.995078 -0.665000 7.906487 -0.665000 5.330078 c +0.665000 5.330078 l +0.665000 7.171948 2.158130 8.665078 4.000000 8.665078 c +4.000000 9.995078 l +h +-0.665000 5.330078 m +-0.665000 2.753670 1.423591 0.665078 4.000000 0.665078 c +4.000000 1.995078 l +2.158130 1.995078 0.665000 3.488208 0.665000 5.330078 c +-0.665000 5.330078 l +h +4.000000 0.665078 m +6.576408 0.665078 8.665000 2.753670 8.665000 5.330078 c +7.335000 5.330078 l +7.335000 3.488208 5.841870 1.995078 4.000000 1.995078 c +4.000000 0.665078 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.000000 8.705566 cm +0.000000 0.000000 0.000000 scn +0.800000 4.694434 m +0.470382 4.941647 0.000000 4.706456 0.000000 4.294434 c +0.000000 0.294434 l +0.000000 -0.117589 0.470382 -0.352780 0.800000 -0.105567 c +3.466667 1.894433 l +3.733333 2.094434 3.733333 2.494434 3.466667 2.694434 c +0.800000 4.694434 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1311 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 12.000000 14.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001401 00000 n +0000001424 00000 n +0000001597 00000 n +0000001671 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1730 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/Contents.json new file mode 100644 index 0000000000..865478dba9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "miniplay.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/miniplay.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/miniplay.pdf new file mode 100644 index 0000000000..62377ab473 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/miniplay.pdf @@ -0,0 +1,73 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.000000 2.780273 cm +0.000000 0.000000 0.000000 scn +2.332051 -0.225573 m +7.127887 2.971651 l +8.018486 3.565384 8.018486 4.874069 7.127886 5.467803 c +2.332050 8.665027 l +1.335218 9.329581 0.000000 8.614994 0.000000 7.416951 c +0.000000 1.022502 l +0.000000 -0.175541 1.335219 -0.890127 2.332051 -0.225573 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 379 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 12.000000 14.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000469 00000 n +0000000491 00000 n +0000000664 00000 n +0000000738 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +797 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json new file mode 100644 index 0000000000..9ca42ca52a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "viewonce_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf new file mode 100644 index 0000000000..cc28878464 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf @@ -0,0 +1,212 @@ +%PDF-1.7 + +1 0 obj + << /ExtGState << /E4 << /ca 0.200000 >> + /E2 << /ca 0.600000 >> + /E3 << /ca 0.400000 >> + /E1 << /ca 0.800000 >> + >> >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 3.670105 cm +0.000000 0.000000 0.000000 scn +10.000000 0.664955 m +10.367270 0.664955 10.665000 0.962687 10.665000 1.329956 c +10.665000 1.697226 10.367270 1.994957 10.000000 1.994957 c +10.000000 0.664955 l +h +10.000000 20.664955 m +10.367270 20.664955 10.665000 20.962687 10.665000 21.329956 c +10.665000 21.697226 10.367270 21.994957 10.000000 21.994957 c +10.000000 20.664955 l +h +10.000000 1.994957 m +4.844422 1.994957 0.665000 6.174377 0.665000 11.329956 c +-0.665000 11.329956 l +-0.665000 5.439838 4.109883 0.664955 10.000000 0.664955 c +10.000000 1.994957 l +h +0.665000 11.329956 m +0.665000 16.485535 4.844422 20.664955 10.000000 20.664955 c +10.000000 21.994957 l +4.109883 21.994957 -0.665000 17.220074 -0.665000 11.329956 c +0.665000 11.329956 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 13.462646 4.335022 cm +0.000000 0.000000 0.000000 scn +0.607792 21.290033 m +0.914249 21.316515 1.224271 21.330017 1.537287 21.330017 c +1.850302 21.330017 2.160324 21.316515 2.466781 21.290033 c +2.832687 21.258417 3.103682 20.936161 3.072065 20.570255 c +3.040448 20.204350 2.718192 19.933353 2.352286 19.964972 c +2.083856 19.988165 1.812035 20.000017 1.537287 20.000017 c +1.262538 20.000017 0.990717 19.988165 0.722287 19.964972 c +0.356381 19.933353 0.034125 20.204350 0.002508 20.570255 c +-0.029109 20.936161 0.241886 21.258417 0.607792 21.290033 c +h +0.002508 0.759779 m +0.034125 1.125685 0.356381 1.396679 0.722287 1.365063 c +0.990717 1.341867 1.262538 1.330017 1.537287 1.330017 c +1.812035 1.330017 2.083856 1.341867 2.352286 1.365063 c +2.718192 1.396679 3.040448 1.125685 3.072065 0.759779 c +3.103682 0.393873 2.832687 0.071617 2.466781 0.039999 c +2.160324 0.013519 1.850302 0.000015 1.537287 0.000015 c +1.224271 0.000015 0.914249 0.013519 0.607792 0.039999 c +0.241886 0.071617 -0.029109 0.393873 0.002508 0.759779 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 18.562012 5.086609 cm +0.000000 0.000000 0.000000 scn +2.718522 2.103071 m +2.929436 1.802403 2.856679 1.387682 2.556010 1.176766 c +2.050149 0.821909 1.511930 0.509789 0.946597 0.245714 c +0.613840 0.090281 0.218083 0.234030 0.062649 0.566786 c +-0.092785 0.899544 0.050963 1.295300 0.383720 1.450733 c +0.878228 1.681723 1.349272 1.954861 1.792216 2.265581 c +2.092885 2.476498 2.507605 2.403738 2.718522 2.103071 c +h +f* +n +Q +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 22.527100 8.415771 cm +0.000000 0.000000 0.000000 scn +1.819443 2.959568 m +2.152200 2.804134 2.295949 2.408377 2.140515 2.075620 c +1.876443 1.510287 1.564322 0.972069 1.209464 0.466206 c +0.998547 0.165539 0.583827 0.092780 0.283160 0.303696 c +-0.017509 0.514613 -0.090267 0.929333 0.120648 1.230002 c +0.431370 1.672945 0.704507 2.143989 0.935496 2.638497 c +1.090930 2.971253 1.486687 3.115002 1.819443 2.959568 c +h +f* +n +Q +q +/E2 gs +1.000000 0.000000 -0.000000 1.000000 24.297363 13.404541 cm +0.000000 0.000000 0.000000 scn +0.607792 3.130305 m +0.973697 3.161922 1.295953 2.890927 1.327572 2.525021 c +1.354051 2.218563 1.367555 1.908542 1.367555 1.595526 c +1.367555 1.282510 1.354051 0.972489 1.327572 0.666031 c +1.295953 0.300125 0.973697 0.029130 0.607792 0.060747 c +0.241886 0.092365 -0.029108 0.414621 0.002508 0.780526 c +0.025703 1.048956 0.037553 1.320777 0.037553 1.595526 c +0.037553 1.870275 0.025703 2.142096 0.002508 2.410526 c +-0.029108 2.776431 0.241886 3.098687 0.607792 3.130305 c +h +f* +n +Q +q +/E3 gs +1.000000 0.000000 -0.000000 1.000000 22.527100 18.379028 cm +0.000000 0.000000 0.000000 scn +0.283159 2.901568 m +0.583828 3.112484 0.998548 3.039725 1.209465 2.739057 c +1.564321 2.233195 1.876442 1.694978 2.140516 1.129645 c +2.295950 0.796888 2.152200 0.401131 1.819444 0.245697 c +1.486686 0.090263 1.090931 0.234012 0.935497 0.566768 c +0.704508 1.061275 0.431369 1.532320 0.120649 1.975264 c +-0.090267 2.275931 -0.017508 2.690652 0.283159 2.901568 c +h +f* +n +Q +q +/E4 gs +1.000000 0.000000 -0.000000 1.000000 18.562012 22.344177 cm +0.000000 0.000000 0.000000 scn +0.062649 2.002510 m +0.218083 2.335267 0.613840 2.479015 0.946597 2.323581 c +1.511930 2.059509 2.050148 1.747388 2.556011 1.392531 c +2.856678 1.181615 2.929437 0.766894 2.718521 0.466226 c +2.507604 0.165558 2.092884 0.092800 1.792215 0.303715 c +1.349271 0.614436 0.878228 0.887573 0.383720 1.118563 c +0.050964 1.273997 -0.092785 1.669754 0.062649 2.002510 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 12.500000 9.737305 cm +0.000000 0.000000 0.000000 scn +2.718506 0.000000 m +2.324219 0.000000 2.075195 0.290527 2.075195 0.715942 c +2.075195 8.443481 l +1.099854 7.727539 l +0.913086 7.571899 0.809326 7.530396 0.601807 7.530396 c +0.269775 7.530396 0.000000 7.820923 0.000000 8.163330 c +0.000000 8.401978 0.114136 8.588745 0.352783 8.765137 c +1.774292 9.730103 l +2.147827 9.979126 2.355347 10.072510 2.645874 10.072510 c +3.112793 10.072510 3.372192 9.792358 3.372192 9.263184 c +3.372192 0.715942 l +3.372192 0.280151 3.123169 0.000000 2.718506 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 4922 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000216 00000 n +0000005194 00000 n +0000005217 00000 n +0000005390 00000 n +0000005464 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5523 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf new file mode 100644 index 0000000000..ea8526a585 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf @@ -0,0 +1,163 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R + /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] + /Domain [ 0.000000 1.000000 ] + /FunctionType 4 + >> +stream +{ 0.419608 exch 0.576471 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub 0.392998 mul 0.419608 add exch dup 0.000000 sub -0.321544 mul 0.576471 add exch dup 0.000000 sub 0.000000 mul 1.000000 add exch } if dup 0.439058 gt { exch pop exch pop exch pop dup 0.439058 sub 0.538311 mul 0.592157 add exch dup 0.439058 sub -0.034955 mul 0.435294 add exch dup 0.439058 sub -0.342561 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.894118 exch 0.415686 exch 0.807843 exch } if pop } +endstream +endobj + +2 0 obj + 533 +endobj + +3 0 obj + << /Pattern << /P1 << /Matrix [ 105.698799 22.310228 -22.310228 105.698799 -4.867018 -86.125580 ] + /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] + /ColorSpace /DeviceRGB + /Function 1 0 R + /Domain [ 0.000000 1.000000 ] + /ShadingType 2 + /Extend [ true true ] + >> + /PatternType 2 + /Type /Pattern + >> >> >> +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +q +1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm +0.949020 0.949020 0.968627 scn +22.340000 15.320000 m +22.340000 9.609375 17.710625 4.980000 12.000000 4.980000 c +12.000000 1.660000 l +19.544210 1.660000 25.660000 7.775789 25.660000 15.320000 c +22.340000 15.320000 l +h +12.000000 4.980000 m +6.289376 4.980000 1.660000 9.609375 1.660000 15.320000 c +-1.660000 15.320000 l +-1.660000 7.775789 4.455791 1.660000 12.000000 1.660000 c +12.000000 4.980000 l +h +1.660000 15.320000 m +1.660000 21.030624 6.289376 25.660000 12.000000 25.660000 c +12.000000 28.980000 l +4.455791 28.980000 -1.660000 22.864208 -1.660000 15.320000 c +1.660000 15.320000 l +h +12.000000 25.660000 m +17.710625 25.660000 22.340000 21.030624 22.340000 15.320000 c +25.660000 15.320000 l +25.660000 22.864208 19.544210 28.980000 12.000000 28.980000 c +12.000000 25.660000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm +0.850980 0.850980 0.850980 scn +24.000000 15.320000 m +24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c +5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c +0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c +18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm +/Pattern cs +/P1 scn +24.000000 15.320000 m +24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c +5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c +0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c +18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c +h +f +n +Q +Q +q +1.000000 0.000000 -0.000000 1.000000 8.793701 5.165123 cm +1.000000 1.000000 1.000000 scn +6.639333 9.995974 m +6.270643 9.995974 5.989172 10.325375 6.046674 10.689552 c +6.848394 15.767110 l +6.948168 16.399014 6.121798 16.727470 5.760551 16.199497 c +0.105843 7.934922 l +-0.166615 7.536714 0.118531 6.996113 0.601028 6.996113 c +3.087691 6.996113 l +3.456380 6.996113 3.737850 6.666713 3.680349 6.302535 c +2.878629 1.224977 l +2.778855 0.593074 3.605225 0.264616 3.966471 0.792590 c +9.621180 9.057164 l +9.893639 9.455373 9.608493 9.995974 9.125995 9.995974 c +6.639333 9.995974 l +h +f* +n +Q + +endstream +endobj + +5 0 obj + 2168 +endobj + +6 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 27.319946 27.320000 ] + /Resources 3 0 R + /Contents 4 0 R + /Parent 7 0 R + >> +endobj + +7 0 obj + << /Kids [ 6 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +8 0 obj + << /Pages 7 0 R + /Type /Catalog + >> +endobj + +xref +0 9 +0000000000 65535 f +0000000010 00000 n +0000000727 00000 n +0000000749 00000 n +0000001379 00000 n +0000003603 00000 n +0000003626 00000 n +0000003799 00000 n +0000003873 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 8 0 R + /Size 9 +>> +startxref +3932 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json new file mode 100644 index 0000000000..17f972537b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AvatarBoost.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf new file mode 100644 index 0000000000..a6b8db9df8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf @@ -0,0 +1,79 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.889648 0.842155 cm +1.000000 1.000000 1.000000 scn +11.065588 16.659954 m +10.451106 16.659954 9.981988 17.208954 10.077824 17.815918 c +11.413990 26.278299 l +11.580280 27.331470 10.202996 27.878902 9.600920 26.998945 c +0.176406 13.224656 l +-0.277692 12.560975 0.197552 11.659972 1.001714 11.659972 c +5.146065 11.659972 l +5.760547 11.659972 6.229665 11.110973 6.133829 10.504009 c +4.797663 2.041628 l +4.631372 0.988457 6.008657 0.441025 6.610733 1.320982 c +16.035248 15.095271 l +16.489344 15.758952 16.014101 16.659954 15.209939 16.659954 c +11.065588 16.659954 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 637 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000727 00000 n +0000000749 00000 n +0000000922 00000 n +0000000996 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1055 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json new file mode 100644 index 0000000000..675ff0268b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Boost.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json new file mode 100644 index 0000000000..45508776ad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SmallBoost.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf new file mode 100644 index 0000000000..fd5f51667d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf @@ -0,0 +1,79 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.259766 0.561462 cm +1.000000 1.000000 1.000000 scn +7.377057 11.106611 m +6.967402 11.106611 6.654656 11.472610 6.718547 11.877253 c +7.609325 17.518847 l +7.720185 18.220961 6.801996 18.585918 6.400612 17.999279 c +0.117604 8.816422 l +-0.185128 8.373968 0.131700 7.773299 0.667809 7.773299 c +3.430712 7.773299 l +3.840366 7.773299 4.153112 7.407299 4.089221 7.002657 c +3.198443 1.361063 l +3.087583 0.658949 4.005772 0.293991 4.407156 0.880630 c +10.690165 10.063488 l +10.992896 10.505941 10.676067 11.106611 10.139959 11.106611 c +7.377057 11.106611 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 622 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 13.333374 20.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000712 00000 n +0000000734 00000 n +0000000907 00000 n +0000000981 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1040 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json new file mode 100644 index 0000000000..4553569f18 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Copy.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf new file mode 100644 index 0000000000..ba20ebd885 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf @@ -0,0 +1,402 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 9.000000 7.000000 cm +1.000000 1.000000 1.000000 scn +1.092019 13.782013 m +1.519843 14.000000 2.079895 14.000000 3.200000 14.000000 c +3.674516 14.000000 l +4.163698 14.000000 4.408288 14.000000 4.638463 13.944740 c +4.842536 13.895746 5.037625 13.814938 5.216570 13.705280 c +5.418403 13.581597 5.591354 13.408646 5.937256 13.062744 c +5.937259 13.062741 l +10.062741 8.937258 l +10.408645 8.591354 10.581596 8.418404 10.705280 8.216570 c +10.814938 8.037625 10.895746 7.842536 10.944740 7.638463 c +11.000000 7.408288 11.000000 7.163698 11.000000 6.674517 c +11.000000 3.200000 l +11.000000 2.079895 11.000000 1.519842 10.782013 1.092019 c +10.590266 0.715694 10.284306 0.409734 9.907981 0.217987 c +9.480158 0.000000 8.920105 0.000000 7.800001 0.000000 c +3.200000 0.000000 l +2.079895 0.000000 1.519843 0.000000 1.092019 0.217987 c +0.715695 0.409734 0.409734 0.715694 0.217987 1.092019 c +0.000000 1.519842 0.000000 2.079895 0.000000 3.200000 c +0.000000 10.800000 l +0.000000 11.920105 0.000000 12.480158 0.217987 12.907981 c +0.409734 13.284306 0.715695 13.590266 1.092019 13.782013 c +h +5.000000 11.896446 m +5.000000 9.000000 l +5.000000 8.447716 5.447715 8.000000 6.000000 8.000000 c +8.896446 8.000000 l +9.119173 8.000000 9.230715 8.269285 9.073223 8.426777 c +5.426776 12.073224 l +5.269285 12.230715 5.000000 12.119173 5.000000 11.896446 c +h +f* +n +Q +q +q +1.000000 0.000000 -0.000000 1.000000 4.000000 0.330414 cm +0.529412 0.490196 1.000000 scn +1.092019 16.451599 m +0.488212 17.636639 l +0.488212 17.636639 l +1.092019 16.451599 l +h +4.638463 16.614326 m +4.327981 15.321074 l +4.327981 15.321074 l +4.638463 16.614326 l +h +5.216570 16.374866 m +4.521646 15.240855 l +4.521647 15.240855 l +5.216570 16.374866 l +h +5.937256 15.732330 m +6.931310 16.615934 l +6.914003 16.635405 6.896128 16.654364 6.877707 16.672783 c +5.937256 15.732330 l +h +5.937259 15.732327 m +4.943204 14.848723 l +4.960511 14.829253 4.978386 14.810296 4.996807 14.791876 c +5.937259 15.732327 l +h +10.062741 11.606844 m +9.122289 10.666392 l +9.122290 10.666392 l +10.062741 11.606844 l +h +10.705280 10.886156 m +11.839292 11.581079 l +11.839291 11.581080 l +10.705280 10.886156 l +h +10.944740 10.308050 m +12.237993 10.618530 l +12.237991 10.618536 l +10.944740 10.308050 l +h +10.782013 3.761605 m +9.596974 4.365414 l +9.596973 4.365412 l +10.782013 3.761605 l +h +9.907981 2.887573 m +9.304174 4.072613 l +9.304173 4.072612 l +9.907981 2.887573 l +h +1.092019 2.887573 m +1.695827 4.072612 l +1.695826 4.072612 l +1.092019 2.887573 l +h +0.217987 3.761605 m +1.403026 4.365412 l +1.403025 4.365413 l +0.217987 3.761605 l +h +0.217987 15.577567 m +-0.967052 16.181376 l +-0.967052 16.181376 l +0.217987 15.577567 l +h +9.073223 11.096363 m +8.132771 10.155910 l +8.132772 10.155910 l +9.073223 11.096363 l +h +5.426776 14.742810 m +6.367229 15.683262 l +6.367229 15.683262 l +5.426776 14.742810 l +h +3.200000 17.999586 m +2.661894 17.999586 2.178326 18.000620 1.778100 17.967920 c +1.362347 17.933952 0.920866 17.857086 0.488212 17.636639 c +1.695826 15.266561 l +1.696496 15.266901 1.699535 15.268437 1.706597 15.270958 c +1.713946 15.273581 1.727419 15.277899 1.749235 15.283036 c +1.795082 15.293834 1.870400 15.306599 1.994708 15.316755 c +2.261491 15.338552 2.618001 15.339586 3.200000 15.339586 c +3.200000 17.999586 l +h +3.674516 17.999586 m +3.200000 17.999586 l +3.200000 15.339586 l +3.674516 15.339586 l +3.674516 17.999586 l +h +4.948946 17.907578 m +4.530101 18.008133 4.100392 17.999586 3.674516 17.999586 c +3.674516 15.339586 l +4.227004 15.339586 4.286476 15.331038 4.327981 15.321074 c +4.948946 17.907578 l +h +5.911493 17.508879 m +5.613550 17.691458 5.288726 17.826004 4.948946 17.907578 c +4.327981 15.321074 l +4.396346 15.304661 4.461700 15.277590 4.521646 15.240855 c +5.911493 17.508879 l +h +6.877707 16.672783 m +6.576569 16.973921 6.278763 17.283815 5.911493 17.508879 c +4.521647 15.240855 l +4.558042 15.218552 4.606139 15.182544 4.996805 14.791878 c +6.877707 16.672783 l +h +6.931313 16.615932 m +6.931310 16.615934 l +4.943202 14.848727 l +4.943204 14.848723 l +6.931313 16.615932 l +h +11.003194 12.547297 m +6.877711 16.672779 l +4.996807 14.791876 l +9.122289 10.666392 l +11.003194 12.547297 l +h +11.839291 11.581080 m +11.614226 11.948351 11.304334 12.246157 11.003193 12.547297 c +9.122290 10.666392 l +9.512956 10.275725 9.548966 10.227628 9.571270 10.191232 c +11.839291 11.581080 l +h +12.237991 10.618536 m +12.156417 10.958313 12.021872 11.283136 11.839292 11.581079 c +9.571269 10.191234 l +9.608004 10.131287 9.635076 10.065931 9.651489 9.997564 c +12.237991 10.618536 l +h +12.330000 9.344103 m +12.330000 9.769979 12.338548 10.199686 12.237993 10.618530 c +9.651487 9.997570 l +9.661452 9.956062 9.670000 9.896589 9.670000 9.344103 c +12.330000 9.344103 l +h +12.330000 5.869586 m +12.330000 9.344103 l +9.670000 9.344103 l +9.670000 5.869586 l +12.330000 5.869586 l +h +11.967052 3.157797 m +12.187500 3.590451 12.264366 4.031933 12.298335 4.447685 c +12.331035 4.847912 12.330000 5.331480 12.330000 5.869586 c +9.670000 5.869586 l +9.670000 5.287587 9.668965 4.931077 9.647168 4.664294 c +9.637012 4.539987 9.624248 4.464668 9.613450 4.418821 c +9.608313 4.397006 9.603994 4.383533 9.601372 4.376184 c +9.598851 4.369123 9.597316 4.366082 9.596974 4.365414 c +11.967052 3.157797 l +h +10.511787 1.702534 m +11.138369 2.021792 11.647794 2.531218 11.967052 3.157799 c +9.596973 4.365412 l +9.532739 4.239344 9.430243 4.136847 9.304174 4.072613 c +10.511787 1.702534 l +h +7.800001 1.339586 m +8.338107 1.339586 8.821674 1.338552 9.221901 1.371251 c +9.637653 1.405220 10.079135 1.482086 10.511789 1.702535 c +9.304173 4.072612 l +9.303504 4.072270 9.300464 4.070735 9.293403 4.068214 c +9.286054 4.065592 9.272580 4.061274 9.250765 4.056136 c +9.204918 4.045339 9.129600 4.032574 9.005292 4.022418 c +8.738509 4.000621 8.381999 3.999586 7.800001 3.999586 c +7.800001 1.339586 l +h +3.200000 1.339586 m +7.800001 1.339586 l +7.800001 3.999586 l +3.200000 3.999586 l +3.200000 1.339586 l +h +0.488211 1.702535 m +0.920865 1.482086 1.362347 1.405220 1.778099 1.371251 c +2.178326 1.338552 2.661894 1.339586 3.200000 1.339586 c +3.200000 3.999586 l +2.618001 3.999586 2.261491 4.000621 1.994708 4.022418 c +1.870400 4.032574 1.795082 4.045339 1.749235 4.056136 c +1.727419 4.061274 1.713946 4.065592 1.706597 4.068214 c +1.699536 4.070735 1.696496 4.072271 1.695827 4.072612 c +0.488211 1.702535 l +h +-0.967052 3.157799 m +-0.647793 2.531218 -0.138368 2.021792 0.488212 1.702535 c +1.695826 4.072612 l +1.569757 4.136847 1.467261 4.239344 1.403026 4.365412 c +-0.967052 3.157799 l +h +-1.330000 5.869586 m +-1.330000 5.331480 -1.331034 4.847912 -1.298335 4.447685 c +-1.264366 4.031933 -1.187500 3.590451 -0.967052 3.157798 c +1.403025 4.365413 l +1.402685 4.366082 1.401149 4.369122 1.398628 4.376184 c +1.396005 4.383532 1.391687 4.397006 1.386550 4.418821 c +1.375753 4.464668 1.362988 4.539987 1.352831 4.664294 c +1.331034 4.931077 1.330000 5.287587 1.330000 5.869586 c +-1.330000 5.869586 l +h +-1.330000 13.469586 m +-1.330000 5.869586 l +1.330000 5.869586 l +1.330000 13.469586 l +-1.330000 13.469586 l +h +-0.967052 16.181376 m +-1.187500 15.748720 -1.264366 15.307240 -1.298335 14.891487 c +-1.331034 14.491261 -1.330000 14.007692 -1.330000 13.469586 c +1.330000 13.469586 l +1.330000 14.051584 1.331034 14.408095 1.352831 14.674878 c +1.362988 14.799186 1.375753 14.874504 1.386550 14.920351 c +1.391687 14.942167 1.396005 14.955641 1.398629 14.962990 c +1.401149 14.970051 1.402685 14.973091 1.403026 14.973760 c +-0.967052 16.181376 l +h +0.488212 17.636639 m +-0.138368 17.317379 -0.647793 16.807955 -0.967052 16.181376 c +1.403026 14.973760 l +1.467261 15.099829 1.569758 15.202326 1.695826 15.266561 c +0.488212 17.636639 l +h +6.330000 11.669586 m +6.330000 14.566032 l +3.670000 14.566032 l +3.670000 11.669586 l +6.330000 11.669586 l +h +6.000000 11.999586 m +6.182254 11.999586 6.330000 11.851840 6.330000 11.669586 c +3.670000 11.669586 l +3.670000 10.382763 4.713177 9.339586 6.000000 9.339586 c +6.000000 11.999586 l +h +8.896446 11.999586 m +6.000000 11.999586 l +6.000000 9.339586 l +8.896446 9.339586 l +8.896446 11.999586 l +h +8.132772 10.155910 m +7.452406 10.836273 7.934273 11.999586 8.896446 11.999586 c +8.896446 9.339586 l +10.304073 9.339586 11.009024 11.041470 10.013674 12.036817 c +8.132772 10.155910 l +h +4.486324 13.802358 m +8.132771 10.155910 l +10.013675 12.036816 l +6.367229 15.683262 l +4.486324 13.802358 l +h +6.330000 14.566032 m +6.330000 13.603863 5.166691 13.121991 4.486324 13.802358 c +6.367229 15.683262 l +5.371879 16.678612 3.670000 15.973656 3.670000 14.566032 c +6.330000 14.566032 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 4.000000 0.330414 cm +1.000000 1.000000 1.000000 scn +1.092019 16.451599 m +1.519843 16.669586 2.079895 16.669586 3.200000 16.669586 c +3.674516 16.669586 l +4.163698 16.669586 4.408288 16.669586 4.638463 16.614326 c +4.842536 16.565332 5.037625 16.484524 5.216570 16.374866 c +5.418403 16.251183 5.591354 16.078232 5.937256 15.732330 c +5.937259 15.732327 l +10.062741 11.606844 l +10.408645 11.260941 10.581596 11.087990 10.705280 10.886156 c +10.814938 10.707211 10.895746 10.512122 10.944740 10.308050 c +11.000000 10.077875 11.000000 9.833284 11.000000 9.344103 c +11.000000 5.869586 l +11.000000 4.749481 11.000000 4.189428 10.782013 3.761605 c +10.590266 3.385281 10.284306 3.079320 9.907981 2.887573 c +9.480158 2.669586 8.920105 2.669586 7.800001 2.669586 c +3.200000 2.669586 l +2.079895 2.669586 1.519843 2.669586 1.092019 2.887573 c +0.715695 3.079320 0.409734 3.385281 0.217987 3.761605 c +0.000000 4.189428 0.000000 4.749481 0.000000 5.869586 c +0.000000 13.469586 l +0.000000 14.589691 0.000000 15.149744 0.217987 15.577567 c +0.409734 15.953892 0.715695 16.259853 1.092019 16.451599 c +h +5.000000 14.566032 m +5.000000 11.669586 l +5.000000 11.117302 5.447715 10.669586 6.000000 10.669586 c +8.896446 10.669586 l +9.119173 10.669586 9.230715 10.938871 9.073223 11.096363 c +5.426776 14.742810 l +5.269285 14.900301 5.000000 14.788759 5.000000 14.566032 c +h +f* +n +Q +Q + +endstream +endobj + +3 0 obj + 9865 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000009955 00000 n +0000009978 00000 n +0000010151 00000 n +0000010225 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +10284 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 751d5d04c2..68b5fc39cd 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -243,6 +243,8 @@ public final class AccountContextImpl: AccountContext { private var userLimitsConfigurationDisposable: Disposable? public private(set) var userLimits: EngineConfiguration.UserLimits + public private(set) var isPremium: Bool + public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false) { self.sharedContextImpl = sharedContext @@ -250,6 +252,7 @@ public final class AccountContextImpl: AccountContext { self.engine = TelegramEngine(account: account) self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue) + self.isPremium = false self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) @@ -380,14 +383,19 @@ public final class AccountContextImpl: AccountContext { }) self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId)) - |> mapToSignal { peer -> Signal in - return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: peer?.isPremium ?? false)) + |> mapToSignal { peer -> Signal<(Bool, EngineConfiguration.UserLimits), NoError> in + let isPremium = peer?.isPremium ?? false + return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: isPremium)) + |> map { userLimits in + return (isPremium, userLimits) + } } - |> deliverOnMainQueue).start(next: { [weak self] value in + |> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in guard let strongSelf = self else { return } - strongSelf.userLimits = value + strongSelf.isPremium = isPremium + strongSelf.userLimits = userLimits }) } @@ -399,6 +407,7 @@ public final class AccountContextImpl: AccountContext { self.countriesConfigurationDisposable?.dispose() self.experimentalUISettingsDisposable?.dispose() self.animatedEmojiStickersDisposable?.dispose() + self.userLimitsConfigurationDisposable?.dispose() } public func storeSecureIdPassword(password: String) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 351c71e2b8..c16aa83df5 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4162,7 +4162,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.dismissInput() let botName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - + let botAddress = peer.addressName ?? "" + if source == .generic { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { return $0.updatedTitlePanelContext { @@ -4216,6 +4217,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: true, fromAttachMenu: false, isInline: false, isSimple: false) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + }, requestSwitchInline: { [weak self] query, chatTypes, completion in + if let strongSelf = self { + if let chatTypes { + let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + controller.peerSelected = { [weak self, weak controller] peer, _ in + if let strongSelf = self { + completion() + controller?.dismiss() + strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil) + } + } + strongSelf.push(controller) + } else { + strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) + } + } }, getInputContainerNode: { [weak self] in if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass { return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, { @@ -4260,7 +4277,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G botAddress = bot.addressName ?? "" } - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, inline: isInline, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme)) + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme)) |> afterDisposed { updateProgress() }) @@ -13098,7 +13115,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case bot(id: PeerId, payload: String?, justInstalled: Bool) case gift } - //editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?, botId: PeerId? = nil, botPayload: String? = nil, botJustInstalled: Bool = false + private func presentAttachmentMenu(subject: AttachMenuSubject) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return @@ -13266,9 +13283,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { let _ = (context.engine.messages.getAttachMenuBot(botId: botId) |> deliverOnMainQueue).start(next: { bot in - let peer = EnginePeer(bot.peer) - - let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 9ab5f25b15..cfbd1db7e2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -542,10 +542,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { switch preparePosition { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if let count = webpageGalleryMediaCount { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string)) + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil) skipStandardStatus = isImage } else if let mediaBadge = mediaBadge { - additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge)) + additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil) } else { skipStandardStatus = isFile } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index a731df238b..dc621a2614 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -80,9 +80,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { private var automaticDownload: Bool? var media: TelegramMediaFile? var appliedForwardInfo: (Peer?, String?)? - - private var secretProgressIcon: UIImage? - + private let fetchDisposable = MetaDisposable() private var durationBackgroundNode: NavigationBackgroundNode? @@ -259,9 +257,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let theme = item.presentationData.theme let isSecretMedia = item.message.containsSecretMedia - var secretProgressIcon: UIImage? if isSecretMedia { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme.theme) secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty) } @@ -575,7 +571,6 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { strongSelf.item = item strongSelf.videoFrame = displayVideoFrame strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) - strongSelf.secretProgressIcon = secretProgressIcon strongSelf.automaticDownload = automaticDownload @@ -1155,11 +1150,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true) } case .Local: - if isSecretMedia && self.secretProgressIcon != nil { + if isSecretMedia { if let (beginTime, timeout) = secretBeginTimeAndTimeout { - state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true) + state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) } else { - state = .customIcon(secretProgressIcon!) + state = .staticTimeout } } else { state = .none diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index f48f5dcaf8..004244ec8b 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) @@ -860,13 +862,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let maxWidth: CGFloat if isSecretMedia { - maxWidth = 180.0 + maxWidth = 200.0 } else { maxWidth = maxDimensions.width } - if isSecretMedia { - let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(presentationData.theme.theme) - } return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in var resultWidth: CGFloat @@ -898,7 +897,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio switch sizeCalculation { case .constrained: if isSecretMedia { - boundingSize = CGSize(width: maxWidth, height: maxWidth) + boundingSize = CGSize(width: maxWidth, height: maxWidth / 5.0 * 3.0) drawingSize = nativeSize.aspectFilled(boundingSize) } else { let fittedSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) @@ -1149,7 +1148,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if isSecretMedia { updateImageSignal = { synchronousLoad, _ in - return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true) } } else { updateImageSignal = { synchronousLoad, highQuality in @@ -1764,7 +1763,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - let radialStatusSize: CGFloat = wideLayout ? 50.0 : 32.0 + var radialStatusSize: CGFloat + if isSecretMedia { + radialStatusSize = 48.0 + } else { + radialStatusSize = wideLayout ? 50.0 : 32.0 + } if progressRequired { if self.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor) @@ -1782,14 +1786,21 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } + let messageTheme = theme.chat.message + var state: RadialStatusNodeState = .none + var backgroundColor = messageTheme.mediaOverlayControlColors.fillColor var badgeContent: ChatMessageInteractiveMediaBadgeContent? var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? - let messageTheme = theme.chat.message + + if isSecretMedia { + backgroundColor = messageTheme.mediaDateAndStatusFillColor + } + if let invoice = invoice { if let extendedMedia = invoice.extendedMedia { if case let .preview(_, _, maybeVideoDuration) = extendedMedia, let videoDuration = maybeVideoDuration { - badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: stringForDuration(videoDuration, position: nil))) + badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: stringForDuration(videoDuration, position: nil)), iconName: nil) } } else { let string = NSMutableAttributedString() @@ -1808,7 +1819,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } string.append(NSAttributedString(string: title)) } - badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string) + badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string, iconName: nil) } } var animated = animated @@ -1944,7 +1955,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } else { let progressString = String(format: "%d%%", Int(progress * 100.0)) - badgeContent = .text(inset: message.flags.contains(.Unsent) ? 0.0 : 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: progressString)) + badgeContent = .text(inset: message.flags.contains(.Unsent) ? 0.0 : 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: progressString), iconName: nil) mediaDownloadState = automaticPlayback ? .none : .compactFetching(progress: 0.0) } @@ -1972,16 +1983,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } case .Local: state = .none - let secretProgressIcon: UIImage? - if case .constrained = sizeCalculation { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) - } else { - secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme) - } - if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime { - state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true) - } else if isSecretMedia, let secretProgressIcon = secretProgressIcon { - state = .customIcon(secretProgressIcon) + if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime, Int32(timeout) != viewOnceTimeout { + state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) + } else if isSecretMedia { + state = .staticTimeout } else if let file = media as? TelegramMediaFile, !file.isVideoSticker { let isInlinePlayableVideo = file.isVideo && !isSecretMedia && (self.automaticPlayback ?? false) if (!isInlinePlayableVideo || isStory) && file.isVideo { @@ -2017,11 +2022,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { if isMediaStreamable(message: message, media: file) { state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) - badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil) mediaDownloadState = .compactRemote } else { state = automaticPlayback ? .none : state - badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil) } } } @@ -2031,16 +2036,32 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout { - let remainingTime: Int32 - if let beginTime = maybeBeginTime { - let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime - remainingTime = Int32(max(0.0, timeout - elapsedTime)) + if isSecretMedia { + let remainingTime: Int32? + if let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, Int32(timeout) != viewOnceTimeout { + if let beginTime = maybeBeginTime { + let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime + remainingTime = Int32(max(0.0, timeout - elapsedTime)) + } else { + remainingTime = Int32(timeout) + } } else { - remainingTime = Int32(timeout) + if let attribute = message.autoclearAttribute { + remainingTime = attribute.timeout + } else if let attribute = message.autoremoveAttribute { + remainingTime = attribute.timeout + } else { + remainingTime = nil + } } - badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: strings.MessageTimer_ShortSeconds(Int32(remainingTime)))) + if let remainingTime { + if remainingTime == viewOnceTimeout { + badgeContent = .text(inset: 10.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: "1"), iconName: "Chat/Message/SecretMediaOnce") + } else { + badgeContent = .text(inset: 10.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: strings.MessageTimer_ShortSeconds(Int32(remainingTime))), iconName: "Chat/Message/SecretMediaPlay") + } + } } if let statusNode = self.statusNode { @@ -2062,6 +2083,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio statusNode?.removeFromSupernode() } }) + statusNode.backgroundNodeColor = backgroundColor } if let badgeContent = badgeContent { if self.badgeNode == nil { @@ -2100,11 +2122,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio displaySpoiler = true } else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) { displaySpoiler = true + } else if isSecretMedia { + displaySpoiler = true } 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() @@ -2115,11 +2139,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio self.extendedMediaOverlayNode?.frame = self.imageNode.frame var tappable = false - switch state { - case .play, .pause, .download, .none: - tappable = true - default: - break + if !isSecretMedia { + switch state { + case .play, .pause, .download, .none: + tappable = true + default: + break + } } self.extendedMediaOverlayNode?.isUserInteractionEnabled = tappable diff --git a/submodules/TelegramUI/Sources/GridMessageItem.swift b/submodules/TelegramUI/Sources/GridMessageItem.swift index 5d6c0903fb..20db9157d1 100644 --- a/submodules/TelegramUI/Sources/GridMessageItem.swift +++ b/submodules/TelegramUI/Sources/GridMessageItem.swift @@ -281,16 +281,16 @@ final class GridMessageItemNode: GridItemNode { switch status { case let .Fetching(_, progress): let progressString = String(format: "%d%%", Int(progress * 100.0)) - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil) mediaDownloadState = .compactFetching(progress: 0.0) case .Local: - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) case .Remote, .Paused: - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) mediaDownloadState = .compactRemote } } else { - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) } strongSelf.mediaBadgeNode.update(theme: item.theme, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ac60a8d053..336b5cb04e 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -675,8 +675,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur |> deliverOnMainQueue).start(next: { bot in let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes) - let botPeer = EnginePeer(bot.peer) - let controller = addWebAppToAttachmentController(context: context, peerName: botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError) @@ -705,7 +704,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur guard let navigationController else { return } - let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true)) + let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true)) } navigationController.pushViewController(controller) } @@ -717,7 +716,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur return } - let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true)) + let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true)) }) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index d81fe3fa4c..affa2df636 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -132,6 +132,7 @@ final class TelegramGlobalSettings { let unreadTrendingStickerPacks: Int let archivedStickerPacks: [ArchivedStickerPackItem]? let userLimits: EngineConfiguration.UserLimits + let bots: [AttachMenuBot] let hasPassport: Bool let hasWatchApp: Bool let enableQRLogin: Bool @@ -153,6 +154,7 @@ final class TelegramGlobalSettings { unreadTrendingStickerPacks: Int, archivedStickerPacks: [ArchivedStickerPackItem]?, userLimits: EngineConfiguration.UserLimits, + bots: [AttachMenuBot], hasPassport: Bool, hasWatchApp: Bool, enableQRLogin: Bool @@ -173,6 +175,7 @@ final class TelegramGlobalSettings { self.unreadTrendingStickerPacks = unreadTrendingStickerPacks self.archivedStickerPacks = archivedStickerPacks self.userLimits = userLimits + self.bots = bots self.hasPassport = hasPassport self.hasWatchApp = hasWatchApp self.enableQRLogin = enableQRLogin @@ -508,9 +511,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, return automaticEnergyUsageShouldBeOn(settings: settings) } |> distinctUntilChanged, - hasStories + hasStories, + context.engine.messages.attachMenuBots() ) - |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories -> PeerInfoScreenData in + |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (featuredStickerPacks, archivedStickerPacks) = stickerPacks @@ -550,6 +554,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, userLimits: peer?.isPremium == true ? limits.1 : limits.0, + bots: bots, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 172e03f3fb..ea7a57cc25 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen import StoryContainerScreen import ChatAvatarNavigationNode import PeerReportScreen +import WebUI enum PeerInfoAvatarEditingMode { case generic @@ -549,6 +550,7 @@ private final class PeerInfoInteraction { let toggleForumTopics: (Bool) -> Void let displayTopicsLimited: (TopicsLimitedReason) -> Void let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void + let openBotApp: (AttachMenuBot) -> Void init( openUsername: @escaping (String) -> Void, @@ -599,7 +601,8 @@ private final class PeerInfoInteraction { dismissInput: @escaping () -> Void, toggleForumTopics: @escaping (Bool) -> Void, displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void, - openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void + openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void, + openBotApp: @escaping (AttachMenuBot) -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -650,6 +653,7 @@ private final class PeerInfoInteraction { self.toggleForumTopics = toggleForumTopics self.displayTopicsLimited = displayTopicsLimited self.openPeerMention = openPeerMention + self.openBotApp = openBotApp } } @@ -661,7 +665,7 @@ private enum SettingsSection: Int, CaseIterable { case phone case accounts case proxy - case stories + case apps case shortcuts case advanced case payment @@ -792,7 +796,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } - items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { + var appIndex = 1000 + if let settings = data.globalSettings { + for bot in settings.bots { + if bot.flags.contains(.showInSettings) { + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: bot.peer.compactDisplayTitle, icon: PresentationResourcesSettings.passport, action: { + interaction.openBotApp(bot) + })) + appIndex += 1 + } + } + } + + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { interaction.openSettings(.stories) })) @@ -2340,6 +2356,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openPeerMention: { [weak self] mention, navigation in self?.openPeerMention(mention, navigation: navigation) + }, + openBotApp: { [weak self] bot in + self?.openBotApp(bot) } ) @@ -4398,7 +4417,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, contentContext: nil) } - private func openUrl(url: String, concealed: Bool, external: Bool) { + private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) { openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { [weak self] tempResolved in @@ -4408,8 +4427,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, openPeer: { peer, navigation in self?.openPeer(peerId: peer.id, navigation: navigation) + commit() }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, @@ -4579,6 +4599,51 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) } + private let openBotAppDisposable = MetaDisposable() + private func openBotApp(_ bot: AttachMenuBot) { + guard let controller = self.controller else { + return + } + + let proceed = { [weak self] in + guard let self else { + return + } + self.openBotAppDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme)) + |> afterDisposed { +// updateProgress() + }) + |> deliverOnMainQueue).start(next: { [weak self] url in + guard let self else { + return + } + let params = WebAppParameters(peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: true) + let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in + self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit) + }, requestSwitchInline: { _, _, _ in + }, getNavigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + }) + controller.navigationPresentation = .flatModal + self.controller?.push(controller) + }, error: { [weak self] error in + if let self { + self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } + + if bot.flags.contains(.showInSettingsDisclaimer) { + let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, peer: bot.peer, completion: { + proceed() + }) + controller.present(alertController, in: .window(.root)) + } else { + proceed() + } + } + private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) { guard let controller = self.controller else { return diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index 0f606a494e..974594da51 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -385,16 +385,16 @@ private final class VisualMediaItemNode: ASDisplayNode { switch status { case let .Fetching(_, progress): let progressString = String(format: "%d%%", Int(progress * 100.0)) - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil) mediaDownloadState = .compactFetching(progress: 0.0) case .Local: - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) case .Remote, .Paused: - badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) mediaDownloadState = .compactRemote } } else { - badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil) } strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 9fdc0e3ac1..0ea89466da 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -39,6 +39,7 @@ import AttachmentTextInputPanelNode import ChatEntityKeyboardInputNode import HashtagSearchUI import PeerInfoStoryGridScreen +import LegacyMessageInputPanel private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1624,98 +1625,107 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { - var presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) +// var presentationData = context.sharedContext.currentPresentationData.with { $0 } +// presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) +// +// var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) +// +// var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? +// var ensureFocusedImpl: (() -> Void)? +// +// let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in +// updateChatPresentationInterfaceStateImpl?({ +// let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode) +// return $0.updatedInterfaceState { interfaceState in +// return interfaceState.withUpdatedEffectiveInputState(updatedState) +// }.updatedInputMode({ _ in updatedMode }) +// }) +// }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in +// updateChatPresentationInterfaceStateImpl?({ +// let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) +// return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ +// $0.withUpdatedMessageActionsState({ value in +// var value = value +// value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId +// return value +// }) +// }) +// }) +// }, openLinkEditing: { +// var selectionRange: Range? +// var text: NSAttributedString? +// var inputMode: ChatInputMode? +// updateChatPresentationInterfaceStateImpl?({ state in +// selectionRange = state.interfaceState.effectiveInputState.selectionRange +// if let selectionRange = selectionRange { +// text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) +// } +// inputMode = state.inputMode +// return state +// }) +// +// var link: String? +// if let text { +// text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in +// if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { +// link = linkAttribute.url +// } +// } +// } +// +// let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in +// if let inputMode = inputMode, let selectionRange = selectionRange { +// if let link = link { +// updateChatPresentationInterfaceStateImpl?({ +// return $0.updatedInterfaceState({ +// $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) +// }) +// }) +// } +// ensureFocusedImpl?() +// updateChatPresentationInterfaceStateImpl?({ +// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ +// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) +// }) +// }) +// } +// }) +// present(controller) +// }) +// +// let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in +// presentInGlobalOverlay(c) +// }, makeEntityInputView: { +// return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) +// }) +// inputPanelNode.interfaceInteraction = interfaceInteraction +// inputPanelNode.effectivePresentationInterfaceState = { +// return presentationInterfaceState +// } +// +// updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in +// let updatedPresentationInterfaceState = f(presentationInterfaceState) +// let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState +// +// presentationInterfaceState = updatedPresentationInterfaceState +// +// if let inputPanelNode = inputPanelNode, updateInputTextState { +// inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true) +// } +// } +// +// ensureFocusedImpl = { [weak inputPanelNode] in +// inputPanelNode?.ensureFocused() +// } +// +// return inputPanelNode - var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil) - - var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? - var ensureFocusedImpl: (() -> Void)? - - let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in - updateChatPresentationInterfaceStateImpl?({ - let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode) - return $0.updatedInterfaceState { interfaceState in - return interfaceState.withUpdatedEffectiveInputState(updatedState) - }.updatedInputMode({ _ in updatedMode }) - }) - }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in - updateChatPresentationInterfaceStateImpl?({ - let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) - return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ - $0.withUpdatedMessageActionsState({ value in - var value = value - value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId - return value - }) - }) - }) - }, openLinkEditing: { - var selectionRange: Range? - var text: NSAttributedString? - var inputMode: ChatInputMode? - updateChatPresentationInterfaceStateImpl?({ state in - selectionRange = state.interfaceState.effectiveInputState.selectionRange - if let selectionRange = selectionRange { - text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) - } - inputMode = state.inputMode - return state - }) - - var link: String? - if let text { - text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in - if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { - link = linkAttribute.url - } - } - } - - let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in - if let inputMode = inputMode, let selectionRange = selectionRange { - if let link = link { - updateChatPresentationInterfaceStateImpl?({ - return $0.updatedInterfaceState({ - $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) - }) - }) - } - ensureFocusedImpl?() - updateChatPresentationInterfaceStateImpl?({ - return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ - $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) - }) - }) - } - }) - present(controller) - }) - - let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in - presentInGlobalOverlay(c) - }, makeEntityInputView: { - return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) - }) - inputPanelNode.interfaceInteraction = interfaceInteraction - inputPanelNode.effectivePresentationInterfaceState = { - return presentationInterfaceState - } - - updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in - let updatedPresentationInterfaceState = f(presentationInterfaceState) - let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState - - presentationInterfaceState = updatedPresentationInterfaceState - - if let inputPanelNode = inputPanelNode, updateInputTextState { - inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true) - } - } - - ensureFocusedImpl = { [weak inputPanelNode] in - inputPanelNode?.ensureFocused() - } + let inputPanelNode = LegacyMessageInputPanelNode( + context: context, + chatLocation: chatLocation, + present: present, + presentInGlobalOverlay: presentInGlobalOverlay + ) return inputPanelNode } diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index da91ad8b42..40c9847038 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -111,6 +111,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private let text: TooltipScreen.Text private let textAlignment: TooltipScreen.Alignment private let balancedTextLayout: Bool + private let constrainWidth: CGFloat? private let tooltipStyle: TooltipScreen.Style private let arrowStyle: TooltipScreen.ArrowStyle private let icon: TooltipScreen.Icon? @@ -159,6 +160,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment, balancedTextLayout: Bool, + constrainWidth: CGFloat?, style: TooltipScreen.Style, arrowStyle: TooltipScreen.ArrowStyle, icon: TooltipScreen.Icon? = nil, @@ -379,6 +381,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.text = text self.textAlignment = textAlignment self.balancedTextLayout = balancedTextLayout + self.constrainWidth = constrainWidth self.animatedStickerNode = DefaultAnimatedStickerNodeImpl() switch icon { @@ -477,7 +480,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } else { animationSize = CGSize(width: 32.0, height: 32.0) } - if animationName == "anim_autoremove_on" { + if ["anim_autoremove_on", "anim_autoremove_off"].contains(animationName) { animationOffset = -3.0 } else if animationName == "ChatListFoldersTooltip" { animationInset = (70.0 - animationSize.width) / 2.0 @@ -491,7 +494,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { animationSpacing = 8.0 } - let containerWidth = max(100.0, min(layout.size.width - sideInset * 2.0, 614.0)) + var containerWidth = max(100.0, min(layout.size.width - sideInset * 2.0, 614.0)) + if let constrainWidth = self.constrainWidth, constrainWidth > 100.0 { + containerWidth = constrainWidth + } var actionSize: CGSize = .zero @@ -1001,6 +1007,7 @@ public final class TooltipScreen: ViewController { public let text: TooltipScreen.Text public let textAlignment: TooltipScreen.Alignment private let balancedTextLayout: Bool + private let constrainWidth: CGFloat? private let style: TooltipScreen.Style private let arrowStyle: TooltipScreen.ArrowStyle private let icon: TooltipScreen.Icon? @@ -1039,6 +1046,7 @@ public final class TooltipScreen: ViewController { text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment = .natural, balancedTextLayout: Bool = false, + constrainWidth: CGFloat? = nil, style: TooltipScreen.Style = .default, arrowStyle: TooltipScreen.ArrowStyle = .default, icon: TooltipScreen.Icon? = nil, @@ -1056,6 +1064,7 @@ public final class TooltipScreen: ViewController { self.text = text self.textAlignment = textAlignment self.balancedTextLayout = balancedTextLayout + self.constrainWidth = constrainWidth self.style = style self.arrowStyle = arrowStyle self.icon = icon @@ -1123,7 +1132,7 @@ public final class TooltipScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, constrainWidth: self.constrainWidth, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 3b3df76ba3..d003f623e1 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -962,7 +962,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in return ("URL", contents) }), textAlignment: .natural) @@ -1347,8 +1347,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var panelWrapperFrame = CGRect(origin: CGPoint(x: leftMargin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - leftMargin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight)) if case .top = self.placementPosition { - panelFrame.origin.y = insets.top + margin - panelWrapperFrame.origin.y = insets.top + margin + var topInset = insets.top + if topInset.isZero { + topInset = layout.statusBarHeight ?? 44.0 + } + panelFrame.origin.y = topInset + margin + panelWrapperFrame.origin.y = topInset + margin } transition.updateFrame(node: self.panelNode, frame: panelFrame) @@ -1441,7 +1445,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let statusSize: CGFloat = 30.0 transition.updateFrame(node: statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize))) if firstLayout { - statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) + statusNode.transitionToState(.secretTimeout(color: .white, icon: .none, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {}) } } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 93586a1973..7f07bf292d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -38,6 +38,8 @@ public class WebAppCancelButtonNode: ASDisplayNode { public var state: State = .cancel + private var color: UIColor? + private var _theme: PresentationTheme public var theme: PresentationTheme { get { @@ -50,6 +52,24 @@ public class WebAppCancelButtonNode: ASDisplayNode { } private let strings: PresentationStrings + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + self.color = color + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.view.snapshotContentTree() { + snapshotView.frame = self.bounds + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + self.setState(self.state, animated: false, animateScale: false, force: true) + } + public init(theme: PresentationTheme, strings: PresentationStrings) { self._theme = theme self.strings = strings @@ -124,13 +144,15 @@ public class WebAppCancelButtonNode: ASDisplayNode { self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) } + let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor + self.arrowNode.isHidden = state == .cancel - self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: self.theme.rootController.navigationBar.accentTextColor) + self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: color) let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) - self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: self.theme.rootController.navigationBar.accentTextColor) + self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color) if let image = self.arrowNode.image { self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) } @@ -305,7 +327,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return .complete() } |> mapToSignal { bot -> Signal<(FileMediaReference, Bool)?, NoError> in - if let bot = bot, let peerReference = PeerReference(bot.peer) { + if let bot = bot, let peerReference = PeerReference(bot.peer._asPeer()) { var imageFile: TelegramMediaFile? var isPlaceholder = false if let file = bot.icons[.placeholder] { @@ -546,12 +568,16 @@ public final class WebAppController: ViewController, AttachmentContainable { } self.controller?.present(promptController, in: .window(.root)) } - + + private func updateNavigationBarAlpha(transition: ContainedViewLayoutTransition) { + let contentOffset = self.webView?.scrollView.contentOffset.y ?? 0.0 + let backgroundAlpha = min(30.0, contentOffset) / 30.0 + self.controller?.navigationBar?.updateBackgroundAlpha(backgroundAlpha, transition: transition) + } + private var targetContentOffset: CGPoint? func scrollViewDidScroll(_ scrollView: UIScrollView) { - let contentOffset = scrollView.contentOffset.y - self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) - + self.updateNavigationBarAlpha(transition: .immediate) if let targetContentOffset = self.targetContentOffset, scrollView.contentOffset != targetContentOffset { scrollView.contentOffset = targetContentOffset } @@ -821,8 +847,14 @@ public final class WebAppController: ViewController, AttachmentContainable { transition.updateBackgroundColor(node: self.backgroundNode, color: color) } case "web_app_set_header_color": - if let json = json, let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { - self.headerColorKey = colorKey + if let json = json { + if let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { + self.headerColor = nil + self.headerColorKey = colorKey + } else if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) { + self.headerColor = color + self.headerColorKey = nil + } self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) } case "web_app_open_popup": @@ -938,16 +970,37 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate var needDismissConfirmation = false + fileprivate var headerColor: UIColor? + fileprivate var headerPrimaryTextColor: UIColor? private var headerColorKey: String? + private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) { + guard let controller = self.controller else { + return + } + let color: UIColor? + var primaryTextColor: UIColor? + var secondaryTextColor: UIColor? var backgroundColor = self.presentationData.theme.list.plainBackgroundColor var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb { backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor } - if let headerColorKey = self.headerColorKey { + if let headerColor = self.headerColor { + color = headerColor + let textColor = headerColor.lightness > 0.5 ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff) + func calculateSecondaryAlpha(luminance: CGFloat, targetContrast: CGFloat) -> CGFloat { + let targetLuminance = luminance > 0.5 ? 0.0 : 1.0 + let adaptiveAlpha = (luminance - targetLuminance + targetContrast) / targetContrast + return max(0.5, min(0.64, adaptiveAlpha)) + } + + primaryTextColor = textColor + self.headerPrimaryTextColor = textColor + secondaryTextColor = textColor.withAlphaComponent(calculateSecondaryAlpha(luminance: headerColor.lightness, targetContrast: 2.5)) + } else if let headerColorKey = self.headerColorKey { switch headerColorKey { case "bg_color": color = backgroundColor @@ -959,6 +1012,13 @@ public final class WebAppController: ViewController, AttachmentContainable { } else { color = nil } + + self.updateNavigationBarAlpha(transition: transition) + controller.updateNavigationBarTheme(transition: transition) + + controller.titleView?.updateTextColors(primary: primaryTextColor, secondary: secondaryTextColor, transition: transition) + controller.cancelButtonNode.updateColor(primaryTextColor, transition: transition) + controller.moreButtonNode.updateColor(primaryTextColor, transition: transition) transition.updateBackgroundColor(node: self.headerBackgroundNode, color: color ?? .clear) transition.updateBackgroundColor(node: self.topOverscrollNode, color: color ?? .clear) } @@ -1234,8 +1294,8 @@ public final class WebAppController: ViewController, AttachmentContainable { } private var titleView: CounterContollerTitleView? - private let cancelButtonNode: WebAppCancelButtonNode - private let moreButtonNode: MoreButtonNode + fileprivate let cancelButtonNode: WebAppCancelButtonNode + fileprivate let moreButtonNode: MoreButtonNode private let context: AccountContext private let peerId: PeerId @@ -1318,10 +1378,9 @@ public final class WebAppController: ViewController, AttachmentContainable { if let strongSelf = self { strongSelf.presentationData = presentationData - let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: "", close: "")) - strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData) + strongSelf.updateNavigationBarTheme(transition: .immediate) strongSelf.titleView?.theme = presentationData.theme - + strongSelf.cancelButtonNode.theme = presentationData.theme strongSelf.moreButtonNode.theme = presentationData.theme @@ -1341,6 +1400,32 @@ public final class WebAppController: ViewController, AttachmentContainable { self.presentationDataDisposable?.dispose() } + fileprivate func updateNavigationBarTheme(transition: ContainedViewLayoutTransition) { + let navigationBarPresentationData: NavigationBarPresentationData + if let backgroundColor = self.controllerNode.headerColor, let textColor = self.controllerNode.headerPrimaryTextColor { + navigationBarPresentationData = NavigationBarPresentationData( + theme: NavigationBarTheme( + buttonColor: textColor, + disabledButtonColor: textColor, + primaryTextColor: textColor, + backgroundColor: backgroundColor, + enableBackgroundBlur: true, + separatorColor: UIColor(rgb: 0x000000, alpha: 0.25), + badgeBackgroundColor: .clear, + badgeStrokeColor: .clear, + badgeTextColor: .clear + ), + strings: NavigationBarStrings(back: "", close: "") + ) + } else { + navigationBarPresentationData = NavigationBarPresentationData( + theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), + strings: NavigationBarStrings(back: "", close: "") + ) + } + self.navigationBar?.updatePresentationData(navigationBarPresentationData) + } + @objc private func cancelPressed() { if case .back = self.cancelButtonNode.state { self.controllerNode.sendBackButtonEvent() diff --git a/submodules/WebUI/Sources/WebAppTermsAlertController.swift b/submodules/WebUI/Sources/WebAppTermsAlertController.swift new file mode 100644 index 0000000000..81a4e2b6df --- /dev/null +++ b/submodules/WebUI/Sources/WebAppTermsAlertController.swift @@ -0,0 +1,302 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import CheckNode +import Markdown + +private let textFont = Font.regular(13.0) +private let boldTextFont = Font.semibold(13.0) + +private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment) +} + +private final class WebAppTermsAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let title: String + private let text: String + + private let titleNode: ImmediateTextNode + private let textNode: ASTextNode + + private let acceptTermsCheckNode: InteractiveCheckNode + private let acceptTermsLabelNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + var acceptedTerms: Bool = false { + didSet { + self.acceptTermsCheckNode.setSelected(self.acceptedTerms, animated: true) + if let firstAction = self.actionNodes.first { + firstAction.actionEnabled = self.acceptedTerms + } + } + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) { + self.strings = strings + self.title = title + self.text = text + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.textAlignment = .center + + self.textNode = ASTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.maximumNumberOfLines = 0 + + self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) + self.acceptTermsLabelNode = ASTextNode() + self.acceptTermsLabelNode.maximumNumberOfLines = 4 + self.acceptTermsLabelNode.isUserInteractionEnabled = true + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.acceptTermsCheckNode) + self.addSubnode(self.acceptTermsLabelNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + if let firstAction = self.actionNodes.first { + firstAction.actionEnabled = false + } + + self.acceptTermsCheckNode.valueChanged = { [weak self] value in + if let strongSelf = self { + strongSelf.acceptedTerms = !strongSelf.acceptedTerms + } + } + + self.updateTheme(theme) + } + + override func didLoad() { + super.didLoad() + + self.acceptTermsLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))) + } + + @objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) { + if self.acceptTermsCheckNode.isUserInteractionEnabled { + self.acceptedTerms = !self.acceptedTerms + } + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + let text = "I agree to the [Terms of Use]()" + self.acceptTermsLabelNode.attributedText = formattedText(text, color: theme.primaryColor, linkColor: theme.accentColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 17.0) + + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + var entriesHeight: CGFloat = 0.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 48.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + + if self.acceptTermsLabelNode.supernode != nil { + origin.y += 21.0 + entriesHeight += 21.0 + + let checkSize = CGSize(width: 22.0, height: 22.0) + let condensedSize = CGSize(width: size.width - 76.0, height: size.height) + + let spacing: CGFloat = 12.0 + let acceptTermsSize = self.acceptTermsLabelNode.measure(condensedSize) + let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width + let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0) + + transition.updateFrame(node: self.acceptTermsCheckNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize)) + transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize)) + origin.y += acceptTermsSize.height + entriesHeight += acceptTermsSize.height + } + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.vertical + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +public func webAppTermsAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, completion: @escaping () -> Void) -> AlertController { + let theme = defaultDarkColorPresentationTheme + let presentationData: PresentationData + if let updatedPresentationData { + presentationData = updatedPresentationData.initial + } else { + presentationData = context.sharedContext.currentPresentationData.with { $0 } + } + let strings = presentationData.strings + + var dismissImpl: ((Bool) -> Void)? + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Continue", action: { + completion() + dismissImpl?(true) + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + })] + + let title = "Warning" + let text = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue." + + let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index b76554712e..26a8eca621 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -88,6 +88,9 @@ final class WebAppWebView: WKWebView { return point.x > 30.0 } self.allowsBackForwardNavigationGestures = false + if #available(iOS 16.4, *) { + self.isInspectable = true + } handleScriptMessageImpl = { [weak self] message in if let strongSelf = self { diff --git a/versions.json b/versions.json index 7dfc3cd2e1..9ece6371a8 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.0.2", + "app": "10.0.3", "bazel": "6.1.1", "xcode": "14.2" }