From f11c070862b96fa1d7ea7b101c27850ac9ea99b4 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 29 Dec 2023 04:50:22 +0400 Subject: [PATCH] [WIP] View-once audio messages --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 + .../Sources/AccountContext.swift | 2 +- .../Sources/SharedMediaPlayer.swift | 9 +- .../Sources/AttachmentPanel.swift | 2 +- .../ChatPresentationInterfaceState/BUILD | 1 + .../ChatPanelInterfaceInteraction.swift | 9 +- .../ContextUI/Sources/ContextController.swift | 4 +- ...tControllerExtractedPresentationNode.swift | 3 +- .../Sources/ContextSourceContainer.swift | 34 ++- .../Display/Source/Nodes/ASImageNode.swift | 4 - .../Sources/InstantPageMediaPlaylist.swift | 10 +- .../Sources/MediaPickerSelectedListNode.swift | 4 +- .../Sources/PhotoResources.swift | 2 +- .../Sources/PremiumBoostScreen.swift | 7 +- .../Sources/PremiumLimitScreen.swift | 3 +- ...RadialStatusSecretTimeoutContentNode.swift | 1 - ...manticStatusNodeSecretTimeoutContext.swift | 8 +- .../BubbleSettingsController.swift | 8 +- .../ForwardPrivacyChatPreviewItem.swift | 2 +- .../TextSizeSelectionController.swift | 8 +- .../Themes/ThemePreviewControllerNode.swift | 2 +- .../Themes/ThemeSettingsChatPreviewItem.swift | 2 +- .../Sources/TelegramBaseController.swift | 7 +- .../ApiUtils/StoreMessage_Telegram.swift | 10 +- .../PendingMessages/EnqueueMessage.swift | 9 + .../ManagedAutoremoveMessageOperations.swift | 10 +- ...SyncCore_TelegramMediaExpiredContent.swift | 2 + ...essageContentAsConsumedInteractively.swift | 20 +- .../TelegramNotices/Sources/Notices.swift | 101 +++++++ .../Sources/DefaultDayPresentationTheme.swift | 4 +- .../Sources/MessageContentKind.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 4 + .../Sources/AudioWaveformComponent.swift | 145 +++++++++- .../Sources/ChatMessageBubbleItemNode.swift | 2 +- .../ChatMessageInteractiveFileNode.swift | 18 +- .../BUILD | 1 + ...atMessageInteractiveInstantVideoNode.swift | 57 +++- .../Sources/ChatRecentActionsController.swift | 2 +- .../InstantVideoRadialStatusNode.swift | 119 +++++++- .../Sources/DrawingMessageRenderer.swift | 2 +- .../Sources/MediaEditorScreen.swift | 3 + .../Sources/MediaScrubberComponent.swift | 1 + .../Sources/MediaPreviewPanelComponent.swift | 2 + .../Sources/PeerInfoScreen.swift | 2 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../PeerNameColorChatPreviewItem.swift | 2 +- .../Sources/ReactionChatPreviewItem.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 2 +- .../Sources/WallpaperGalleryItem.swift | 6 +- .../ViewOnceon.imageset/1filled.pdf | 85 ++++++ .../ViewOnceon.imageset/Contents.json | 12 + .../Resources/Animations/anim_flame_1.tgs | Bin 0 -> 5377 bytes .../Resources/Animations/anim_flame_2.tgs | Bin 0 -> 4985 bytes .../TelegramUI/Sources/ChatController.swift | 72 ++++- .../Sources/ChatControllerNode.swift | 11 +- ...essageContextControllerContentSource.swift | 250 ++++++++++++++++ .../ChatRecordingPreviewInputPanelNode.swift | 268 +++++++++++++++++- .../Sources/ChatTextInputPanelNode.swift | 97 ++++++- .../Sources/OverlayPlayerControlsNode.swift | 2 +- .../Sources/PeerMessagesMediaPlaylist.swift | 2 +- .../Sources/SharedAccountContext.swift | 4 +- .../Sources/SharedMediaPlayer.swift | 8 +- 62 files changed, 1351 insertions(+), 131 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/1filled.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Resources/Animations/anim_flame_1.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/anim_flame_2.tgs diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3b083c16a7..fd450f9821 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10841,3 +10841,12 @@ Sorry for the inconvenience."; "RequestPeer.ReachedMaximum_any" = "You can select up to %@ users."; "ChatList.DeleteSavedPeerConfirmation" = "Are you sure you want to delete saved messages from %@?"; + +"Message.VoiceMessageExpired" = "Expired voice message"; +"Message.VideoMessageExpired" = "Expired video message"; + +"Chat.PlayOnceVoiceMessageTooltip" = "This voice message can only be played once."; +"Chat.PlayOnceVoiceMessageYourTooltip" = "This message will disappear once **%@** plays it once."; + +"Chat.TapToPlayVoiceMessageOnceTooltip" = "Tap to set this message to **Play Once**"; +"Chat.PlayVoiceMessageOnceTooltip" = "The recipients will be able to listen to it only once."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 9b920f112c..eb0bbc5839 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -896,7 +896,7 @@ public protocol SharedAccountContext: AnyObject { selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode ) -> ChatHistoryListNode - func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? diff --git a/submodules/AccountContext/Sources/SharedMediaPlayer.swift b/submodules/AccountContext/Sources/SharedMediaPlayer.swift index 0beadc3572..71d26fc45e 100644 --- a/submodules/AccountContext/Sources/SharedMediaPlayer.swift +++ b/submodules/AccountContext/Sources/SharedMediaPlayer.swift @@ -12,18 +12,21 @@ public enum SharedMediaPlaybackDataType { } public enum SharedMediaPlaybackDataSource: Equatable { - case telegramFile(reference: FileMediaReference, isCopyProtected: Bool) + case telegramFile(reference: FileMediaReference, isCopyProtected: Bool, isViewOnce: Bool) public static func ==(lhs: SharedMediaPlaybackDataSource, rhs: SharedMediaPlaybackDataSource) -> Bool { switch lhs { - case let .telegramFile(lhsFileReference, lhsIsCopyProtected): - if case let .telegramFile(rhsFileReference, rhsIsCopyProtected) = rhs { + case let .telegramFile(lhsFileReference, lhsIsCopyProtected, lhsIsViewOnce): + if case let .telegramFile(rhsFileReference, rhsIsCopyProtected, rhsIsViewOnce) = rhs { if !lhsFileReference.media.isEqual(to: rhsFileReference.media) { return false } if lhsIsCopyProtected != rhsIsCopyProtected { return false } + if lhsIsViewOnce != rhsIsViewOnce { + return false + } return true } else { return false diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 3e107d4c80..ee7d84d87d 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -827,7 +827,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/ChatPresentationInterfaceState/BUILD b/submodules/ChatPresentationInterfaceState/BUILD index e8c07bfa74..5d3ea70cf2 100644 --- a/submodules/ChatPresentationInterfaceState/BUILD +++ b/submodules/ChatPresentationInterfaceState/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/ChatContextQuery", + "//submodules/TooltipUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index a9952ef608..dc0e5e9e51 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -7,6 +7,7 @@ import TelegramCore import Display import AccountContext import ContextUI +import TooltipUI public enum ChatLoadingMessageSubject { case generic @@ -16,7 +17,7 @@ public enum ChatLoadingMessageSubject { public enum ChatFinishMediaRecordingAction { case dismiss case preview - case send + case send(viewOnce: Bool) } public final class ChatPanelInterfaceInteractionStatuses { @@ -108,7 +109,7 @@ public final class ChatPanelInterfaceInteraction { public let stopMediaRecording: () -> Void public let lockMediaRecording: () -> Void public let deleteRecordedMedia: () -> Void - public let sendRecordedMedia: (Bool) -> Void + public let sendRecordedMedia: (Bool, Bool) -> Void public let displayRestrictedInfo: (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void public let displayVideoUnmuteTip: (CGPoint?) -> Void public let switchMediaRecordingMode: () -> Void @@ -214,7 +215,7 @@ public final class ChatPanelInterfaceInteraction { stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, - sendRecordedMedia: @escaping (Bool) -> Void, + sendRecordedMedia: @escaping (Bool, Bool) -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, @@ -431,7 +432,7 @@ public final class ChatPanelInterfaceInteraction { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 2e8f08408c..024be3fb41 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2215,13 +2215,15 @@ public final class ContextController: ViewController, StandalonePresentableContr public let source: ContextContentSource public let items: Signal public let closeActionTitle: String? + public let closeAction: (() -> Void)? - public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal, closeActionTitle: String? = nil) { + public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal, closeActionTitle: String? = nil, closeAction: (() -> Void)? = nil) { self.id = id self.title = title self.source = source self.items = items self.closeActionTitle = closeActionTitle + self.closeAction = closeAction } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 936c452ade..9f18b5b3f4 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -1121,13 +1121,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo var animationInContentXDistance: CGFloat = 0.0 let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX - let contentY = contentParentGlobalFrame.minY + contentRect.minY - contentNode.containingItem.contentRect.minY let contentWidth = contentNode.containingItem.view.bounds.size.width let contentHeight = contentNode.containingItem.view.bounds.size.height if case let .extracted(extracted) = self.source, extracted.centerVertically { if actionsSize.height.isZero { let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0) - animationInContentYDistance = fixedContentY - contentY + animationInContentYDistance = fixedContentY - contentRect.minY } else if contentX + contentWidth > layout.size.width / 2.0, actionsSize.height > 0.0 { let fixedContentX = layout.size.width - (contentX + contentWidth) animationInContentXDistance = fixedContentX - contentX diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index 6f4b0816fd..d6c7ec6e7e 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -18,6 +18,7 @@ final class ContextSourceContainer: ASDisplayNode { let title: String let source: ContextContentSource let closeActionTitle: String? + let closeAction: (() -> Void)? private var _presentationNode: ContextControllerPresentationNode? var presentationNode: ContextControllerPresentationNode { @@ -43,13 +44,15 @@ final class ContextSourceContainer: ASDisplayNode { title: String, source: ContextContentSource, items: Signal, - closeActionTitle: String? = nil + closeActionTitle: String? = nil, + closeAction: (() -> Void)? = nil ) { self.controller = controller self.id = id self.title = title self.source = source self.closeActionTitle = closeActionTitle + self.closeAction = closeAction self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get()) |> map { a, b -> Bool in @@ -385,7 +388,8 @@ final class ContextSourceContainer: ASDisplayNode { title: source.title, source: source.source, items: source.items, - closeActionTitle: source.closeActionTitle + closeActionTitle: source.closeActionTitle, + closeAction: source.closeAction ) self.sources.append(mappedSource) self.addSubnode(mappedSource.presentationNode) @@ -472,17 +476,27 @@ final class ContextSourceContainer: ASDisplayNode { } func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + let delayDismissal = self.activeSource?.closeAction != nil + let delay: Double = delayDismissal ? 0.2 : 0.0 + let duration: Double = delayDismissal ? 0.35 : 0.2 + + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false, completion: { _ in + if delayDismissal { + Queue.mainQueue().after(0.55) { + completion() + } + } + }) if let tabSelectorView = self.tabSelector?.view { - tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false) } if let closeButtonView = self.closeButton?.view { - closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false) } if let activeSource = self.activeSource { - activeSource.animateOut(result: result, completion: completion) + activeSource.animateOut(result: result, completion: delayDismissal ? {} : completion) } else { completion() } @@ -670,11 +684,15 @@ final class ContextSourceContainer: ASDisplayNode { ) ), effectAlignment: .center, - action: { [weak self] in + action: { [weak self, weak source] in guard let self else { return } - self.controller?.dismiss(result: .dismissWithoutContent, completion: nil) + if let source, let closeAction = source.closeAction { + closeAction() + } else { + self.controller?.dismiss(result: .dismissWithoutContent, completion: nil) + } }) ), environment: {}, diff --git a/submodules/Display/Source/Nodes/ASImageNode.swift b/submodules/Display/Source/Nodes/ASImageNode.swift index f4b8406aaa..2ebf0e9987 100644 --- a/submodules/Display/Source/Nodes/ASImageNode.swift +++ b/submodules/Display/Source/Nodes/ASImageNode.swift @@ -47,8 +47,4 @@ open class ASImageNode: ASDisplayNode { override public func calculateSizeThatFits(_ contrainedSize: CGSize) -> CGSize { return self.image?.size ?? CGSize() } - - public var asdf: Int { - return 1234 - } } diff --git a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift index 3f7da6b890..11484db8b4 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift @@ -49,13 +49,13 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { switch attribute { case let .Audio(isVoice, _, _, _, _): if isVoice { - return SharedMediaPlaybackData(type: .voice, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .voice, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } else { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } case let .Video(_, _, flags, _): if flags.contains(.instantRoundVideo) { - return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .instantVideo, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } else { return nil } @@ -64,12 +64,12 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { } } if file.mimeType.hasPrefix("audio/") { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } if let fileName = file.fileName { let ext = (fileName as NSString).pathExtension.lowercased() if ext == "wav" || ext == "opus" { - return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false)) + return SharedMediaPlaybackData(type: .music, source: .telegramFile(reference: .webPage(webPage: WebpageReference(self.webPage), media: file), isCopyProtected: false, isViewOnce: false)) } } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 9ab15c0373..f48f1b2b86 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -859,10 +859,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI let previewText = groupLayouts.count > 1 ? presentationData.strings.Attachment_MessagesPreview : presentationData.strings.Attachment_MessagePreview let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: previewText, entities: [], additionalAttributes: nil))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true) + let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true, isPreview: true) let dragMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_DragToReorder, entities: [], additionalAttributes: nil))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true) + let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true, isPreview: true) let headerItems: [ListViewItem] = [previewItem, dragItem] diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 87a14b5113..450d9bb9ad 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -2141,7 +2141,7 @@ public func chatSecretMessageVideo(account: Account, userLocation: MediaResource if blurredImage == nil { if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { let thumbnailSize = CGSize(width: image.width, height: image.height) - let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) + let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 40.0, height: 40.0)) if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift index 748c0f801c..8cffe20868 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -237,8 +237,11 @@ public func PremiumBoostScreen( actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { dismissImpl?() - let controller = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost) - pushController(controller) + + Queue.mainQueue().after(0.4) { + let controller = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost) + pushController(controller) + } }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) ], diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index a1d53d53f2..9b47ab85f8 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1034,8 +1034,9 @@ private final class LimitSheetContent: CombinedComponent { string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgePosition = max(0.15, min(0.85, CGFloat(component.count) / CGFloat(premiumLimit))) badgeGraphPosition = badgePosition + buttonAnimationName = nil if isPremiumDisabled { badgeText = "\(limit)" diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index 03133297ea..cc0bb47138 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -143,7 +143,6 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { return } - let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let alphaProgress: CGFloat diff --git a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift index c281fe9da3..103cb5a5ca 100644 --- a/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift +++ b/submodules/SemanticStatusNode/Sources/SemanticStatusNodeSecretTimeoutContext.swift @@ -68,7 +68,7 @@ final class SemanticStatusNodeSecretTimeoutContext: SemanticStatusNodeStateConte context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.translateBy(x: 4.0, y: 7.0) + context.translateBy(x: 6.0, y: 8.0) context.clip(to: iconRect, mask: iconImage.cgImage!) context.fill(iconRect) context.restoreGState() @@ -218,9 +218,9 @@ private struct ContentParticle { private final class FireIconNode: ManagedAnimationNode { init() { - super.init(size: CGSize(width: 36.0, height: 36.0)) + super.init(size: CGSize(width: 32.0, height: 32.0)) - self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 80), duration: 2.5)) - self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 80, endFrame: 115), duration: 0.85, loop: true)) + self.trackTo(item: ManagedAnimationItem(source: .local("anim_flame_1"), frames: .range(startFrame: 0, endFrame: 60), duration: 1.5)) + self.trackTo(item: ManagedAnimationItem(source: .local("anim_flame_2"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0, loop: true)) } } diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 9dd7708f93..75b5b852b3 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -170,20 +170,20 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 3569643a28..c4190b3f5a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -149,7 +149,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: []) - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 39b8e37855..1e9f8351ef 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -435,20 +435,20 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 9dd1e3c964..c258319120 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -619,7 +619,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { sampleMessages.append(message8) items = sampleMessages.reversed().map { message in - self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil, accountPeer: nil, isCentered: false) + self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true) } let width: CGFloat diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index ee60dca8a3..8e4c892102 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -168,7 +168,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } var nodes: [ListViewItemNode] = [] diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 4538013b8a..2ac5619743 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -643,7 +643,12 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { } } - if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType, !mediaAccessoryPanelHidden { + var isViewOnceMessage = false + if let (item, _, _, _, _, _) = self.playlistStateAndType, let source = item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce { + isViewOnceMessage = true + } + + if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType, !mediaAccessoryPanelHidden && !isViewOnceMessage { let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelStartY), size: CGSize(width: layout.size.width, height: panelHeight)) additionalHeight += panelHeight diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 94136f437f..c77129451d 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -349,7 +349,15 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil) } } else { - return (TelegramMediaExpiredContent(data: .file), nil, nil, nil, nil) + var data: TelegramMediaExpiredContentData + if (flags & (1 << 7)) != 0 { + data = .videoMessage + } else if (flags & (1 << 8)) != 0 { + data = .voiceMessage + } else { + data = .file + } + return (TelegramMediaExpiredContent(data: data), nil, nil, nil, nil) } case let .messageMediaWebPage(flags, webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage) { diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 3601676e83..4c23fe9516 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -145,6 +145,15 @@ public enum EnqueueMessage { return nil } } + + public var attributes: [MessageAttribute] { + switch self { + case let .message(_, attributes, _, _, _, _, _, _, _, _): + return attributes + case let .forward(_, _, _, attributes, _): + return attributes + } + } } private extension EnqueueMessage { diff --git a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift index 9071cf2f79..b480b40be6 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAutoremoveMessageOperations.swift @@ -94,8 +94,14 @@ func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRe 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 file = updatedMedia[i] as? TelegramMediaFile { + if file.isInstantVideo { + updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage) + } else if file.isVoice { + updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage) + } else { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } var updatedAttributes = currentMessage.attributes diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift index cfa0f73ca6..da0dc939cb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaExpiredContent.swift @@ -4,6 +4,8 @@ import Postbox public enum TelegramMediaExpiredContentData: Int32 { case image case file + case voiceMessage + case videoMessage } public final class TelegramMediaExpiredContent: Media, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift index 76d2aacf95..b181922619 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MarkMessageContentAsConsumedInteractively.swift @@ -198,8 +198,14 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M 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 file = updatedMedia[i] as? TelegramMediaFile { + if file.isInstantVideo { + updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage) + } else if file.isVoice { + updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage) + } else { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } } @@ -216,8 +222,14 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M 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) + } else if let file = updatedMedia[i] as? TelegramMediaFile { + if file.isInstantVideo { + updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage) + } else if file.isVoice { + updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage) + } else { + updatedMedia[i] = TelegramMediaExpiredContent(data: .file) + } } } } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 00c6499718..f450a51749 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -189,6 +189,9 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case dismissedPremiumColorsBadge = 55 case multipleReactionsSuggestion = 56 case savedMessagesChatsSuggestion = 57 + case voiceMessagesPlayOnceSuggestion = 58 + case incomingVoiceMessagePlayOnceTip = 59 + case outgoingVoiceMessagePlayOnceTip = 60 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -454,21 +457,38 @@ private struct ApplicationSpecificNoticeKeys { static func dismissedPremiumAppIconsBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumAppIconsBadge.key) } + static func replyQuoteTextSelectionTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.replyQuoteTextSelectionTip.key) } + static func dismissedPremiumWallpapersBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumWallpapersBadge.key) } + static func dismissedPremiumColorsBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumColorsBadge.key) } + static func multipleReactionsSuggestion() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.multipleReactionsSuggestion.key) } + static func savedMessagesChatsSuggestion() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.savedMessagesChatsSuggestion.key) } + + static func voiceMessagesPlayOnceSuggestion() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.voiceMessagesPlayOnceSuggestion.key) + } + + static func incomingVoiceMessagePlayOnceTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.incomingVoiceMessagePlayOnceTip.key) + } + + static func outgoingVoiceMessagePlayOnceTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.outgoingVoiceMessagePlayOnceTip.key) + } } public struct ApplicationSpecificNotice { @@ -1883,4 +1903,85 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getVoiceMessagesPlayOnceSuggestion(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.voiceMessagesPlayOnceSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementVoiceMessagesPlayOnceSuggestion(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.voiceMessagesPlayOnceSuggestion())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.voiceMessagesPlayOnceSuggestion(), entry) + } + + return Int(previousValue) + } + } + + public static func getIncomingVoiceMessagePlayOnceTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.incomingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementIncomingVoiceMessagePlayOnceTip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.incomingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.incomingVoiceMessagePlayOnceTip(), entry) + } + + return Int(previousValue) + } + } + + public static func getOutgoingVoiceMessagePlayOnceTip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.outgoingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementOutgoingVoiceMessagePlayOnceTip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.outgoingVoiceMessagePlayOnceTip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.outgoingVoiceMessagePlayOnceTip(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index b5fcd56959..3f4587e207 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -714,7 +714,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x748391, alpha: 0.45)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: .clear), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: UIColor(rgb: 0xffffff)), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.45), foregroundColor: UIColor(rgb: 0xffffff)), selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(white: 1.0, alpha: 0.6), @@ -857,7 +857,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio shareButtonFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0xffffff, alpha: 0.8)), shareButtonStrokeColor: PresentationThemeVariableColor(withWallpaper: .clear, withoutWallpaper: UIColor(rgb: 0xe5e5ea)), shareButtonForegroundColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), - mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.6), foregroundColor: UIColor(rgb: 0xffffff)), + mediaOverlayControlColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0x000000, alpha: 0.45), foregroundColor: UIColor(rgb: 0xffffff)), selectionControlColors: PresentationThemeFillStrokeForeground(fillColor: defaultDayAccentColor, strokeColor: UIColor(rgb: 0xc7c7cc), foregroundColor: UIColor(rgb: 0xffffff)), deliveryFailedColors: PresentationThemeFillForeground(fillColor: UIColor(rgb: 0xff3b30), foregroundColor: UIColor(rgb: 0xffffff)), mediaHighlightOverlayColor: UIColor(rgb: 0xffffff, alpha: 0.6), diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index f372aa491e..97b7b3d924 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -278,7 +278,7 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil switch expiredMedia.data { case .image: return .expiredImage - case .file: + case .file, .videoMessage, .voiceMessage: return .expiredVideo } case .image: diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 7248c286e5..ed1e6f4ede 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -965,6 +965,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = NSAttributedString(string: strings.Message_ImageExpired, font: titleFont, textColor: primaryTextColor) case .file: attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: primaryTextColor) + case .videoMessage: + attributedString = NSAttributedString(string: strings.Message_VideoMessageExpired, font: titleFont, textColor: primaryTextColor) + case .voiceMessage: + attributedString = NSAttributedString(string: strings.Message_VoiceMessageExpired, font: titleFont, textColor: primaryTextColor) } } else if let _ = media as? TelegramMediaStory { let compactPeerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" diff --git a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift index 73468673bd..03ea1c9261 100644 --- a/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift +++ b/submodules/TelegramUI/Components/AudioWaveformComponent/Sources/AudioWaveformComponent.swift @@ -19,6 +19,7 @@ public final class AudioWaveformComponent: Component { public let samples: Data public let peak: Int32 public let status: Signal + public let isViewOnceMessage: Bool public let seek: ((Double) -> Void)? public let updateIsSeeking: ((Bool) -> Void)? @@ -30,6 +31,7 @@ public final class AudioWaveformComponent: Component { samples: Data, peak: Int32, status: Signal, + isViewOnceMessage: Bool, seek: ((Double) -> Void)?, updateIsSeeking: ((Bool) -> Void)? ) { @@ -40,6 +42,7 @@ public final class AudioWaveformComponent: Component { self.samples = samples self.peak = peak self.status = status + self.isViewOnceMessage = isViewOnceMessage self.seek = seek self.updateIsSeeking = updateIsSeeking } @@ -63,6 +66,9 @@ public final class AudioWaveformComponent: Component { if lhs.peak != rhs.peak { return false } + if lhs.isViewOnceMessage != rhs.isViewOnceMessage { + return false + } return true } @@ -204,6 +210,9 @@ public final class AudioWaveformComponent: Component { private var statusDisposable: Disposable? private var playbackStatusAnimator: ConstantDisplayLinkAnimator? + private var sparksView: SparksView? + private var progress: CGFloat = 0.0 + private var revealProgress: CGFloat = 1.0 private var animator: DisplayLinkAnimator? @@ -391,6 +400,21 @@ public final class AudioWaveformComponent: Component { }) } + if component.isViewOnceMessage { + let sparksView: SparksView + if let current = self.sparksView { + sparksView = current + } else { + sparksView = SparksView() + self.addSubview(sparksView) + self.sparksView = sparksView + } + sparksView.frame = CGRect(origin: .zero, size: size).insetBy(dx: -5.0, dy: -5.0) + } else if let sparksView = self.sparksView { + self.sparksView = nil + sparksView.removeFromSuperview() + } + return size } @@ -408,12 +432,25 @@ public final class AudioWaveformComponent: Component { if needsAnimation != (self.playbackStatusAnimator != nil) { if needsAnimation { self.playbackStatusAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + if let self, let component = self.component, let sparksView = self.sparksView { + sparksView.update(position: CGPoint(x: sparksView.bounds.width * self.progress, y: sparksView.bounds.height / 2.0), color: component.foregroundColor) + } self?.setNeedsDisplay() }) self.playbackStatusAnimator?.isPaused = false + + if let sparksView = self.sparksView { + sparksView.alpha = 1.0 + sparksView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } else { self.playbackStatusAnimator?.invalidate() self.playbackStatusAnimator = nil + + if let sparksView = self.sparksView { + sparksView.alpha = 0.0 + sparksView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } } } } @@ -445,7 +482,7 @@ public final class AudioWaveformComponent: Component { timestampAndDuration = nil } - let playbackProgress: CGFloat + var playbackProgress: CGFloat if let (timestamp, duration) = timestampAndDuration { if let scrubbingTimestampValue = self.scrubbingTimestampValue { var progress = CGFloat(scrubbingTimestampValue / duration) @@ -474,6 +511,10 @@ public final class AudioWaveformComponent: Component { } else { playbackProgress = 0.0 } + if component.isViewOnceMessage { + playbackProgress = 1.0 - playbackProgress + } + self.progress = playbackProgress let sampleWidth: CGFloat = 2.0 let halfSampleWidth: CGFloat = 1.0 @@ -571,7 +612,11 @@ public final class AudioWaveformComponent: Component { } if component.backgroundColor.alpha > 0.0 { - context.setFillColor(component.backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor) + var backgroundColor = component.backgroundColor + if component.isViewOnceMessage { + backgroundColor = component.foregroundColor.withMultipliedAlpha(0.0) + } + context.setFillColor(backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor) } else { context.setFillColor(component.foregroundColor.cgColor) } @@ -604,3 +649,99 @@ public final class AudioWaveformComponent: Component { return view.update(component: self, availableSize: availableSize, transition: transition) } } + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +private class SparksView: UIView { + private var particles: [ContentParticle] = [] + private var color: UIColor = .black + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = nil + self.isOpaque = false + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(position: CGPoint, color: UIColor) { + self.color = color + + let v = CGPoint(x: 1.0, y: 0.0) + let c = CGPoint(x: position.x - 3.0, y: position.y - 5.5 + 13.0 * CGFloat(arc4random_uniform(100)) / 100.0 + 1.0) + + let timestamp = CACurrentMediaTime() + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 2 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(100)) - 50.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (80.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5 + + let lifetime = Double(0.65 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + self.setNeedsDisplay() + } + + override public func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + + context.setFillColor(self.color.cgColor) + + for particle in self.particles { + let size: CGFloat = 1.0 + context.setAlpha(particle.alpha * 1.0) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 7def4f6902..519649f255 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -3849,7 +3849,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var isCurrentlyPlayingMedia = false - if item.associatedData.currentlyPlayingMessageId == item.message.index { + if item.associatedData.currentlyPlayingMessageId == item.message.index, let file = item.message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isInstantVideo { isCurrentlyPlayingMedia = true } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index b180726d8e..863a15f862 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -739,7 +739,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let isViewOnceMessage = "".isEmpty || arguments.message.autoremoveAttribute?.timeout == viewOnceTimeout + let isViewOnceMessage = isVoice && arguments.message.minAutoremoveOrClearTimeout == viewOnceTimeout let fileSizeString: String if let _ = arguments.file.size { @@ -1300,6 +1300,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { samples: audioWaveform?.samples ?? Data(), peak: audioWaveform?.peak ?? 0, status: strongSelf.playbackStatus.get(), + isViewOnceMessage: isViewOnceMessage, seek: { timestamp in if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(EngineMessage(message)) { context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type) @@ -1559,7 +1560,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } } - let isViewOnceMessage = "".isEmpty || (isVoice && message.autoremoveAttribute?.timeout == viewOnceTimeout) + let isViewOnceMessage = isVoice && message.minAutoremoveOrClearTimeout == viewOnceTimeout var state: SemanticStatusNodeState var streamingState: SemanticStatusNodeState = .none @@ -1780,6 +1781,11 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { self.streamingStatusNode = streamingStatusNode streamingStatusNode.frame = streamingCacheStatusFrame self.addSubnode(streamingStatusNode) + + if isViewOnceMessage { + streamingStatusNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + streamingStatusNode.layer.animateAlpha(from: 0.1, to: 1.0, duration: 0.2) + } } else if let streamingStatusNode = self.streamingStatusNode { streamingStatusNode.backgroundNodeColor = backgroundNodeColor } @@ -1798,10 +1804,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } }) - switch state { - case .pause: + if showBlobs { self.playbackAudioLevelNode?.startAnimating() - default: + } else { self.playbackAudioLevelNode?.stopAnimating() } } @@ -1809,6 +1814,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if let streamingStatusNode = self.streamingStatusNode { if streamingState == .none { self.streamingStatusNode = nil + if isViewOnceMessage { + streamingStatusNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in if streamingState == .none { streamingStatusNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD index 6e073b1b22..2e541b8c8d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramNotices", "//submodules/Markdown", "//submodules/TextFormat", + "//submodules/InvisibleInkDustNode", "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index d6ba4acab9..355eab8cf1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -29,6 +29,7 @@ import ChatInstantVideoMessageDurationNode import ChatControllerInteraction import WallpaperBackgroundNode import TelegramStringFormatting +import InvisibleInkDustNode public struct ChatMessageInstantVideoItemLayoutResult { public let contentSize: CGSize @@ -89,6 +90,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { public var audioTranscriptionButton: ComponentHostView? + private var dustNode: MediaDustNode? private var statusNode: RadialStatusNode? private var disappearingStatusNode: RadialStatusNode? private var playbackStatusNode: InstantVideoRadialStatusNode? @@ -278,8 +280,13 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme.theme) } + let isViewOnceMessage = item.message.minAutoremoveOrClearTimeout == viewOnceTimeout + let theme = item.presentationData.theme - let isSecretMedia = item.message.containsSecretMedia + var isSecretMedia = item.message.containsSecretMedia + if isViewOnceMessage { + isSecretMedia = true + } if isSecretMedia { secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty) } @@ -805,9 +812,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } })) } - - let isViewOnceMessage = "".isEmpty - + var displayTranscribe = false if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !isViewOnceMessage && !item.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) @@ -946,12 +951,15 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { animation.animator.updateFrame(layer: strongSelf.secretVideoPlaceholderBackground.layer, frame: displayVideoFrame, completion: nil) let placeholderFrame = videoFrame.insetBy(dx: 2.0, dy: 2.0) - strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) animation.animator.updateScale(layer: strongSelf.secretVideoPlaceholder.layer, scale: imageScale, completion: nil) animation.animator.updatePosition(layer: strongSelf.secretVideoPlaceholder.layer, position: displayVideoFrame.center, completion: nil) + let placeholderSide = floor(placeholderFrame.size.width / 2.0) * 2.0 + let placeholderSize = CGSize(width: placeholderSide, height: placeholderSide) + let makeSecretPlaceholderLayout = strongSelf.secretVideoPlaceholder.asyncLayout() - let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderFrame.size.width / 2.0), imageSize: placeholderFrame.size, boundingSize: placeholderFrame.size, intrinsicInsets: UIEdgeInsets()) + let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderSize.width / 2.0), imageSize: placeholderSize, boundingSize: placeholderSize, intrinsicInsets: UIEdgeInsets()) let applySecretPlaceholder = makeSecretPlaceholderLayout(arguments) applySecretPlaceholder() @@ -1144,7 +1152,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } let messageTheme = item.presentationData.theme.theme.chat.message + let isViewOnceMessage = item.message.minAutoremoveOrClearTimeout == viewOnceTimeout + let isSecretMedia = item.message.containsSecretMedia + var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { if let attribute = item.message.autoclearAttribute { @@ -1197,6 +1208,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.infoBackgroundNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) } } + self.infoBackgroundNode.isHidden = isViewOnceMessage var isBuffering: Bool? if let message = self.item?.message, let media = self.media, isMediaStreamable(message: message, media: media) && (self.automaticDownload ?? false) { @@ -1279,7 +1291,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true) } case .Local: - if isSecretMedia { + if isViewOnceMessage { + state = .play(messageTheme.mediaOverlayControlColors.foregroundColor) + } else if isSecretMedia { if let (beginTime, timeout) = secretBeginTimeAndTimeout { state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true) } else { @@ -1321,7 +1335,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let current = self.playbackStatusNode { playbackStatusNode = current } else { - playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6), hasSeek: true) + playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6), hasSeek: !isViewOnceMessage, sparks: isViewOnceMessage) + playbackStatusNode.isUserInteractionEnabled = !isViewOnceMessage playbackStatusNode.seekTo = { [weak self] position, play in guard let strongSelf = self else { return @@ -1349,6 +1364,11 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.videoNode?.isHidden = false self.secretVideoPlaceholderBackground.isHidden = true self.secretVideoPlaceholder.isHidden = true + + if let dustNode = self.dustNode { + self.dustNode = nil + dustNode.removeFromSupernode() + } } else { if let playbackStatusNode = self.playbackStatusNode { self.playbackStatusNode = nil @@ -1356,9 +1376,24 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } self.durationNode?.status = .single(nil) - self.videoNode?.isHidden = isSecretMedia - self.secretVideoPlaceholderBackground.isHidden = !isSecretMedia - self.secretVideoPlaceholder.isHidden = !isSecretMedia && !item.presentationData.isPreview + self.videoNode?.isHidden = isSecretMedia || isViewOnceMessage + self.secretVideoPlaceholderBackground.isHidden = !isSecretMedia && !isViewOnceMessage + self.secretVideoPlaceholder.isHidden = !isSecretMedia && !isViewOnceMessage && !item.presentationData.isPreview + + if isViewOnceMessage { + let dustNode: MediaDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = MediaDustNode(enableAnimations: item.controllerInteraction.enableFullTranslucency) + dustNode.clipsToBounds = true + self.insertSubnode(dustNode, belowSubnode: self.dateAndStatusNode) + self.dustNode = dustNode + } + dustNode.cornerRadius = videoFrame.width / 2.0 + dustNode.frame = videoFrame + dustNode.update(size: videoFrame.size, color: .white, transition: .immediate) + } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index fae2294c6b..39fa03c8e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -103,7 +103,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift index 2d8a84006c..bd7e982c5d 100644 --- a/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode/Sources/InstantVideoRadialStatusNode.swift @@ -7,6 +7,24 @@ import UniversalMediaPlayer import LegacyComponents import UIKitRuntimeUtils +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + private final class InstantVideoRadialStatusNodeParameters: NSObject { let color: UIColor let progress: CGFloat @@ -14,14 +32,18 @@ private final class InstantVideoRadialStatusNodeParameters: NSObject { let playProgress: CGFloat let blinkProgress: CGFloat let hasSeek: Bool + let sparks: Bool + let particles: [ContentParticle] - init(color: UIColor, progress: CGFloat, dimProgress: CGFloat, playProgress: CGFloat, blinkProgress: CGFloat, hasSeek: Bool) { + init(color: UIColor, progress: CGFloat, dimProgress: CGFloat, playProgress: CGFloat, blinkProgress: CGFloat, hasSeek: Bool, sparks: Bool, particles: [ContentParticle]) { self.color = color self.progress = progress self.dimProgress = dimProgress self.playProgress = playProgress self.blinkProgress = blinkProgress self.hasSeek = hasSeek + self.sparks = sparks + self.particles = particles } } @@ -43,8 +65,12 @@ private extension CGPoint { public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecognizerDelegate { private let color: UIColor private let hasSeek: Bool - private let hapticFeedback = HapticFeedback() + private let sparks: Bool + private let hapticFeedback = HapticFeedback() + + private var particles: [ContentParticle] = [] + private var effectiveProgress: CGFloat = 0.0 { didSet { self.setNeedsDisplay() @@ -85,6 +111,8 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni } } + private var animator: ConstantDisplayLinkAnimator? + private var statusDisposable: Disposable? private var statusValuePromise = Promise() @@ -111,9 +139,10 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni public var seekTo: ((Double, Bool) -> Void)? - public init(color: UIColor, hasSeek: Bool) { + public init(color: UIColor, hasSeek: Bool, sparks: Bool = false) { self.color = color self.hasSeek = hasSeek + self.sparks = sparks super.init() @@ -127,6 +156,13 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni }) self.view.disablesInteractiveTransitionGestureRecognizer = true + + if sparks { + self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateSparks() + }) + self.animator?.isPaused = false + } } deinit { @@ -165,6 +201,60 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni } } + private func updateSparks() { +// let bounds = self.bounds + +// let lineWidth: CGFloat = 4.0 +// let center = CGPoint(x: bounds.midX, y: bounds.midY) +// let radius: CGFloat = (bounds.size.width - lineWidth - 4.0 * 2.0) * 0.5 + + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * self.effectiveProgress + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) +// let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let timestamp = CACurrentMediaTime() + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 70.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (25.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5 + + let lifetime = Double(0.25 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: .zero, direction: direction, velocity: velocity, alpha: 0.8, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + self.setNeedsDisplay() + } + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) let location = gestureRecognizer.location(in: self.view) @@ -259,7 +349,7 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni } override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress, dimProgress: self.effectiveDimProgress, playProgress: self.effectivePlayProgress, blinkProgress: self.effectiveBlinkProgress, hasSeek: self.hasSeek) + return InstantVideoRadialStatusNodeParameters(color: self.color, progress: self.effectiveProgress, dimProgress: self.effectiveDimProgress, playProgress: self.effectivePlayProgress, blinkProgress: self.effectiveBlinkProgress, hasSeek: self.hasSeek, sparks: self.sparks, particles: self.particles) } @objc public override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -296,8 +386,15 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni context.setBlendMode(.normal) var progress = parameters.progress - let startAngle = -CGFloat.pi / 2.0 - let endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + let startAngle: CGFloat + let endAngle: CGFloat + if parameters.sparks { + endAngle = -CGFloat.pi / 2.0 + startAngle = CGFloat(progress) * 2.0 * CGFloat.pi + endAngle + } else { + startAngle = -CGFloat.pi / 2.0 + endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle + } progress = min(1.0, progress) @@ -363,6 +460,16 @@ public final class InstantVideoRadialStatusNode: ASDisplayNode, UIGestureRecogni context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: handleFrame) } + + let v = CGPoint(x: sin(startAngle), y: -cos(startAngle)) + let c = CGPoint(x: -v.y * pathDiameter * 0.5 + bounds.midX, y: v.x * pathDiameter * 0.5 + bounds.midY) + + context.setFillColor(parameters.color.cgColor) + for particle in parameters.particles { + let size: CGFloat = 1.3 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: c.x + particle.position.x - size / 2.0, y: c.y + particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index ba99243372..cd0ba03359 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -187,7 +187,7 @@ public final class DrawingMessageRenderer { let avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[self.messages.first!.author!.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)] + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)] let inset: CGFloat = 16.0 let leftInset: CGFloat = 37.0 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 68b8d42edf..246fc32968 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2448,6 +2448,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let _ = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: nil) { messageFile = maybeFile } + if "".isEmpty { + messageFile = nil + } let renderer = DrawingMessageRenderer(context: self.context, messages: messages) renderer.render(completion: { result in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift index 037d7fa544..9c4132866e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaScrubberComponent.swift @@ -1046,6 +1046,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega samples: samples, peak: peak, status: .complete(), + isViewOnceMessage: false, seek: nil, updateIsSeeking: nil ) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift index dbd4714232..69f49b4767 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MediaPreviewPanelComponent.swift @@ -288,6 +288,7 @@ public final class MediaPreviewPanelComponent: Component { ) } }, + isViewOnceMessage: false, seek: { [weak self] timestamp in guard let self, let mediaPlayer = self.mediaPlayer else { return @@ -318,6 +319,7 @@ public final class MediaPreviewPanelComponent: Component { samples: component.mediaPreview.waveform.samples, peak: component.mediaPreview.waveform.peak, status: .complete(), + isViewOnceMessage: false, seek: nil, updateIsSeeking: nil )), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 48b4d3e486..6490b6d319 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -348,7 +348,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 079cf25593..445edfc04d 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -592,7 +592,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, stopMediaRecording: { }, lockMediaRecording: { }, deleteRecordedMedia: { - }, sendRecordedMedia: { _ in + }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 3133579374..90a0f7db35 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -236,7 +236,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] : [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } var nodes: [ListViewItemNode] = [] diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index 603c48c9ac..cac1f69208 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -292,7 +292,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { attributes.append(ReactionsMessageAttribute(canViewList: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers)) } - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: true) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index a23fef5c55..52018357b5 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -1087,7 +1087,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate return state }, animated: true) }, clickThroughMessage: { - }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false) + }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true) return item } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index 84ecd5cac6..2a423e6c83 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -1622,19 +1622,19 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if !bottomMessageText.isEmpty { let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: messageAttributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) if let serviceMessageText { let attributedText = convertMarkdownToAttributes(NSAttributedString(string: serviceMessageText)) let entities = generateChatInputTextEntities(attributedText) let message3 = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: attributedText.string, entities: entities, additionalAttributes: nil))], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true)) } let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/1filled.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/1filled.pdf new file mode 100644 index 0000000000..19ddecd42f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/1filled.pdf @@ -0,0 +1,85 @@ +%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 4.169922 4.170044 cm +0.000000 0.000000 0.000000 scn +10.830000 21.659973 m +4.848756 21.659973 0.000000 16.811216 0.000000 10.829973 c +0.000000 4.848728 4.848756 -0.000027 10.830000 -0.000027 c +16.811245 -0.000027 21.660000 4.848728 21.660000 10.829973 c +21.660000 16.811216 16.811245 21.659973 10.830000 21.659973 c +h +10.775391 6.581909 m +10.775391 6.048706 11.130859 5.693237 11.643555 5.693237 c +12.156250 5.693237 12.511719 6.048706 12.511719 6.581909 c +12.511719 14.846558 l +12.511719 15.454956 12.135742 15.830933 11.513672 15.830933 c +11.144531 15.830933 10.871094 15.769409 10.433594 15.461792 c +8.416992 14.046753 l +8.123047 13.841675 8.013672 13.636597 8.013672 13.363159 c +8.013672 12.973511 8.280273 12.706909 8.649414 12.706909 c +8.840820 12.706909 8.977539 12.754761 9.148438 12.877808 c +10.741211 13.985229 l +10.775391 13.985229 l +10.775391 6.581909 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 941 +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 +0000000034 00000 n +0000001031 00000 n +0000001053 00000 n +0000001226 00000 n +0000001300 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1359 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/Contents.json new file mode 100644 index 0000000000..bf64e7bd8f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnceon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "1filled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Resources/Animations/anim_flame_1.tgs b/submodules/TelegramUI/Resources/Animations/anim_flame_1.tgs new file mode 100644 index 0000000000000000000000000000000000000000..835556dd0b363cee5bc2ac2f895f20d8265f90b4 GIT binary patch literal 5377 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/anim_flame_2.tgs b/submodules/TelegramUI/Resources/Animations/anim_flame_2.tgs new file mode 100644 index 0000000000000000000000000000000000000000..01661129813aa5be2425883bfb6037a07abaa77b GIT binary patch literal 4985 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 deliverOnMainQueue).startStandalone(next: { [weak self] data in @@ -15593,7 +15602,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, usedCorrelationId ? correlationId : nil) - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + var attributes: [MessageAttribute] = [] + if viewOnce { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) + } + + strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) strongSelf.recorderFeedback?.tap() strongSelf.recorderFeedback = nil @@ -15660,9 +15674,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedRecordedMediaPreview(nil) }) + self.updateDownButtonVisibility() } - func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil) { + func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false) { self.chatDisplayNode.updateRecordedMediaDeleted(false) if let recordedMediaPreview = self.presentationInterfaceState.recordedMediaPreview { @@ -15687,10 +15702,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } }) + + strongSelf.updateDownButtonVisibility() } }, nil) - let messages: [EnqueueMessage] = [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + var attributes: [MessageAttribute] = [] + if viewOnce { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) + } + + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] let transformedMessages: [EnqueueMessage] if let silentPosting = silentPosting { @@ -16008,7 +16030,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func updateDownButtonVisibility() { - let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil + let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil || self.presentationInterfaceState.recordedMediaPreview != nil self.chatDisplayNode.navigateButtons.displayDownButton = self.shouldDisplayDownButton && !recordingMediaMessage } @@ -18923,16 +18945,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func openViewOnceMediaMessage(_ message: Message) { - let source: ContextContentSource = .extracted(ChatMessageContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: false, centerVertically: true)) + let isIncoming = message.effectivelyIncoming(self.context.account.peerId) + var presentImpl: ((ViewController) -> Void)? let configuration = ContextController.Configuration( sources: [ ContextController.Source( id: 0, title: "", - source: source, + source: .extracted(ChatViewOnceMessageContextExtractedContentSource( + context: self.context, + presentationData: self.presentationData, + chatNode: self.chatDisplayNode, + backgroundNode: self.chatBackgroundNode, + engine: self.context.engine, + message: message, + present: { c in + presentImpl?(c) + } + )), items: .single(ContextController.Items(content: .list([]))), - closeActionTitle: "Delete and Close" + closeActionTitle: isIncoming ? "Delete and Close" : "Close", + closeAction: { [weak self] in + if let self { + self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) + } + } ) ], initialId: 0 ) @@ -18947,7 +18985,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.currentContextController = contextController self.presentInGlobalOverlay(contextController) - let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: nil)) + presentImpl = { [weak contextController] c in + contextController?.present(c, in: .current) + } + + let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .singleMessage(message.id))) } func openStorySharing(messages: [Message]) { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 587a58bad4..d161f7e91b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -140,7 +140,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var validEmptyNodeLayout: (CGSize, UIEdgeInsets)? var restrictedNode: ChatRecentActionsEmptyNode? - private var validLayout: (ContainerViewLayout, CGFloat)? + private(set) var validLayout: (ContainerViewLayout, CGFloat)? private var visibleAreaInset = UIEdgeInsets() private var searchNavigationNode: ChatSearchNavigationContentNode? @@ -1417,6 +1417,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelNode.removeFromSupernode() inputPanelNode.prevInputPanelNode = prevInputPanelNode inputPanelNode.addSubnode(prevInputPanelNode) + + prevInputPanelNode.viewForOverlayContent?.removeFromSuperview() } else { dismissedInputPanelNode = self.inputPanelNode } @@ -1426,10 +1428,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if inputPanelNode.supernode !== self { immediatelyLayoutInputPanelAndAnimateAppearance = true self.inputPanelClippingNode.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) - - if let viewForOverlayContent = inputPanelNode.viewForOverlayContent { - self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) - } + } + if let viewForOverlayContent = inputPanelNode.viewForOverlayContent, viewForOverlayContent.superview == nil { + self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) } } else { let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index ff7bf433ff..a1103af4b4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -1,11 +1,18 @@ import Foundation import UIKit +import AsyncDisplayKit import Display import ContextUI import Postbox import TelegramCore import SwiftSignalKit import ChatMessageItemView +import AccountContext +import WallpaperBackgroundNode +import TelegramPresentationData +import DustEffect +import TooltipUI +import TelegramNotices final class ChatMessageContextLocationContentSource: ContextLocationContentSource { private let controller: ViewController @@ -97,6 +104,249 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou } } +final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + let centerVertically: Bool = true + + private let context: AccountContext + private let presentationData: PresentationData + private weak var chatNode: ChatControllerNode? + private weak var backgroundNode: WallpaperBackgroundNode? + private let engine: TelegramEngine + private let message: Message + private let present: (ViewController) -> Void + + private var messageNodeCopy: ChatMessageItemView? + private weak var tooltipController: TooltipScreen? + + var shouldBeDismissed: Signal { + return self.context.sharedContext.mediaManager.globalMediaPlayerState + |> filter { playlistStateAndType in + if let (_, state, _) = playlistStateAndType, case .state = state { + return true + } else { + return false + } + } + |> take(1) + |> map { _ in + return false + } + |> then( + self.context.sharedContext.mediaManager.globalMediaPlayerState + |> filter { playlistStateAndType in + return playlistStateAndType == nil + } + |> take(1) + |> map { _ in + return true + } + ) + } + + init(context: AccountContext, presentationData: PresentationData, chatNode: ChatControllerNode, backgroundNode: WallpaperBackgroundNode, engine: TelegramEngine, message: Message, present: @escaping (ViewController) -> Void) { + self.context = context + self.presentationData = presentationData + self.chatNode = chatNode + self.backgroundNode = backgroundNode + self.engine = engine + self.message = message + self.present = present + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let chatNode = self.chatNode, let backgroundNode = self.backgroundNode, let validLayout = chatNode.validLayout?.0 else { + return nil + } + + var result: ContextControllerTakeViewInfo? + var sourceNode: ContextExtractedContentContainingNode? + var sourceRect: CGRect = .zero + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.message.stableId) { + sourceNode = contentNode + sourceRect = itemNode.frame + } + } + + let isIncoming = self.message.effectivelyIncoming(self.context.account.peerId) + let isVideo = (self.message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile)?.isInstantVideo ?? false + + var tooltipSourceRect: CGRect = .zero + + if let sourceNode { + var bubbleWidth: CGFloat = 0.0 + + if (isIncoming || "".isEmpty) && !isVideo { + let messageItem = self.context.sharedContext.makeChatMessagePreviewItem( + context: self.context, + messages: [self.message], + theme: self.presentationData.theme, + strings: self.presentationData.strings, + wallpaper: self.presentationData.chatWallpaper, + fontSize: self.presentationData.chatFontSize, + chatBubbleCorners: self.presentationData.chatBubbleCorners, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameOrder: self.presentationData.nameDisplayOrder, + forcedResourceStatus: nil, + tapMessage: nil, + clickThroughMessage: nil, + backgroundNode: backgroundNode, + availableReactions: nil, + accountPeer: nil, + isCentered: false, + isPreview: false + ) + + let params = ListViewItemLayoutParams(width: chatNode.historyNode.frame.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, availableHeight: chatNode.historyNode.frame.height, isStandalone: false) + var node: ListViewItemNode? + + messageItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { messageNode, apply in + node = messageNode + apply().1(ListViewItemApply(isOnScreen: true)) + }) + + if let messageNode = node as? ChatMessageItemView, let copyContentNode = messageNode.getMessageContextSourceNode(stableId: self.message.stableId) { + messageNode.frame.origin.y = chatNode.frame.height - sourceRect.origin.y - sourceRect.size.height + chatNode.addSubnode(messageNode) + result = ContextControllerTakeViewInfo(containingItem: .node(copyContentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + + bubbleWidth = copyContentNode.contentNode.subnodes?.first?.frame.width ?? messageNode.frame.width + } + + self.messageNodeCopy = node as? ChatMessageItemView + } else { + result = ContextControllerTakeViewInfo(containingItem: .node(sourceNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + + tooltipSourceRect = CGRect(x: isIncoming ? 22.0 : chatNode.frame.width - bubbleWidth + 10.0, y: floorToScreenPixels((chatNode.frame.height - 75.0) / 2.0) - 43.0, width: 44.0, height: 44.0) + } + + if !isVideo { + let displayTooltip = { [weak self] in + guard let self else { + return + } + let absoluteFrame = tooltipSourceRect + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY), size: CGSize()) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var tooltipText: String? + if isIncoming { + tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageTooltip + } else if let peer = self.message.peers[self.message.id.peerId] { + let peerName = EnginePeer(peer).compactDisplayTitle + tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageYourTooltip(peerName).string + } + + if let tooltipText { + let tooltipController = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .markdown(text: tooltipText), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: nil, + location: .point(location, .bottom), + displayDuration: .custom(3.0), + inset: 8.0, + cornerRadius: 11.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + self.present(tooltipController) + } + } + + if isIncoming { + let _ = (ApplicationSpecificNotice.getIncomingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self else { + return + } + if counter >= 2 { + return + } + Queue.mainQueue().after(0.3) { + displayTooltip() + } + let _ = ApplicationSpecificNotice.incrementIncomingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } else { + let _ = (ApplicationSpecificNotice.getOutgoingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self else { + return + } + if counter >= 2 { + return + } + Queue.mainQueue().after(0.3) { + displayTooltip() + } + let _ = ApplicationSpecificNotice.incrementOutgoingVoiceMessagePlayOnceTip(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } + } + return result + } + + private var dustEffectLayer: DustEffectLayer? + func putBack() -> ContextControllerPutBackViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + if let tooltipController = self.tooltipController { + tooltipController.dismiss() + } + + if let messageNodeCopy = self.messageNodeCopy, let sourceView = messageNodeCopy.supernode?.view, let contentNode = messageNodeCopy.getMessageContextSourceNode(stableId: nil)?.contentNode, let parentNode = contentNode.supernode?.supernode?.supernode { + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = sourceView.bounds.center + dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: sourceView.bounds.size) + dustEffectLayer.zPosition = 10.0 + parentNode.layer.addSublayer(dustEffectLayer) + + guard let (image, subFrame) = messageNodeCopy.makeContentSnapshot() else { + return nil + } + var itemFrame = subFrame //messageNodeCopy.layer.convert(subFrame, to: dustEffectLayer) + itemFrame.origin.y = floorToScreenPixels((sourceView.frame.height - subFrame.height) / 2.0) + dustEffectLayer.addItem(frame: itemFrame, image: image) + messageNodeCopy.removeFromSupernode() + contentNode.removeFromSupernode() + return nil + } else { + var result: ContextControllerPutBackViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + } +} + final class ChatMessageReactionContextExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = true diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index 9ee9c0168b..009619e070 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -15,10 +15,48 @@ import ChatPresentationInterfaceState import ChatSendButtonRadialStatusNode import AudioWaveformNode import ChatInputPanelNode +import TooltipUI +import TelegramNotices extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { } +final class ChatRecordingPreviewViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { + let ignoreHit: (UIView, CGPoint) -> Bool + + init(ignoreHit: @escaping (UIView, CGPoint) -> Bool) { + self.ignoreHit = ignoreHit + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func maybeDismissContent(point: CGPoint) { + for subview in self.subviews.reversed() { + if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) { + return + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.subviews.reversed() { + if let result = subview.hitTest(self.convert(point, to: subview), with: event) { + return result + } + } + + if event == nil || self.ignoreHit(self, point) { + return nil + } + + return nil + } +} + final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { let deleteButton: HighlightableButtonNode let binNode: AnimationNode @@ -29,6 +67,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { private let waveformButton: ASButtonNode let waveformBackgroundNode: ASImageNode + private var viewOnce = false + let viewOnceButton: ChatRecordingViewOnceButtonNode + private let waveformNode: AudioWaveformNode private let waveformForegroundNode: AudioWaveformNode let waveformScubberNode: MediaPlayerScrubbingNode @@ -63,6 +104,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.sendButton.displaysAsynchronously = false self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(theme), for: []) + self.viewOnceButton = ChatRecordingViewOnceButtonNode() + self.waveformBackgroundNode = ASImageNode() self.waveformBackgroundNode.isLayerBacked = true self.waveformBackgroundNode.displaysAsynchronously = false @@ -92,6 +135,21 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { super.init() + self.viewForOverlayContent = ChatRecordingPreviewViewForOverlayContent( + ignoreHit: { [weak self] view, point in + guard let strongSelf = self else { + return false + } + if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil { + return true + } + if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY { + return true + } + return false + } + ) + self.addSubnode(self.deleteButton) self.deleteButton.addSubnode(self.binNode) self.addSubnode(self.waveformBackgroundNode) @@ -111,9 +169,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } } - + self.deleteButton.addTarget(self, action: #selector(self.deletePressed), forControlEvents: [.touchUpInside]) self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: [.touchUpInside]) + self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside]) self.waveformButton.addTarget(self, action: #selector(self.waveformPressed), forControlEvents: .touchUpInside) } @@ -135,9 +194,38 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } strongSelf.interfaceInteraction?.displaySendMessageOptions(strongSelf.sendButton, gesture) } + + if let viewForOverlayContent = self.viewForOverlayContent { + viewForOverlayContent.addSubnode(self.viewOnceButton) + } + } + + private func maybePresentViewOnceTooltip() { + guard let context = self.context else { + return + } + let _ = (ApplicationSpecificNotice.getVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let self, let interfaceState = self.presentationInterfaceState else { + return + } + if counter >= 3 { + return + } + + Queue.mainQueue().after(0.3) { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_TapToPlayVoiceMessageOnceTooltip, hasIcon: true) + } + + let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager).startStandalone() + }) } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + var isFirstTime = false + if self.presentationInterfaceState == nil { + isFirstTime = true + } if self.presentationInterfaceState != interfaceState { var updateWaveform = false if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview { @@ -148,8 +236,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.sendButton.accessibilityLabel = interfaceState.strings.VoiceOver_MessageContextSend self.waveformButton.accessibilityLabel = interfaceState.strings.VoiceOver_Chat_RecordPreviewVoiceMessage } - self.presentationInterfaceState = interfaceState + self.presentationInterfaceState = interfaceState + if let recordedMediaPreview = interfaceState.recordedMediaPreview, updateWaveform { self.waveformNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor.withAlphaComponent(0.5), gravity: .center, waveform: recordedMediaPreview.waveform) self.waveformForegroundNode.setup(color: interfaceState.theme.chat.inputPanel.actionControlForegroundColor, gravity: .center, waveform: recordedMediaPreview.waveform) @@ -182,12 +271,20 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } } - let panelHeight = defaultHeight(metrics: metrics) + if isFirstTime { + self.maybePresentViewOnceTooltip() + } + let panelHeight = defaultHeight(metrics: metrics) + transition.updateFrame(node: self.deleteButton, frame: CGRect(origin: CGPoint(x: leftInset + 2.0 - UIScreenPixel, y: 1), size: CGSize(width: 40.0, height: 40))) transition.updateFrame(node: self.sendButton, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel, y: 2 - UIScreenPixel), size: CGSize(width: 44.0, height: 44))) self.binNode.frame = self.deleteButton.bounds + let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -64.0), size: viewOnceSize) + transition.updateFrame(node: self.viewOnceButton, frame: viewOnceButtonFrame) + var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -229,6 +326,16 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if let prevTextInputPanelNode = self.prevInputPanelNode as? ChatTextInputPanelNode { self.prevInputPanelNode = nil + self.viewOnce = prevTextInputPanelNode.viewOnce + self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) + + prevTextInputPanelNode.viewOnceButton.isHidden = true + prevTextInputPanelNode.viewOnce = false + self.viewOnceButton.layer.animatePosition(from: prevTextInputPanelNode.viewOnceButton.position, to: self.viewOnceButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + prevTextInputPanelNode.viewOnceButton.isHidden = false + prevTextInputPanelNode.viewOnceButton.update(isSelected: false, animated: false) + }) + if let audioRecordingDotNode = prevTextInputPanelNode.audioRecordingDotNode { let startAlpha = CGFloat(audioRecordingDotNode.layer.presentation()?.opacity ?? 1.0) audioRecordingDotNode.layer.removeAllAnimations() @@ -283,12 +390,63 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } @objc func deletePressed() { + self.tooltipController?.dismiss() + self.mediaPlayer?.pause() self.interfaceInteraction?.deleteRecordedMedia() } @objc func sendPressed() { - self.interfaceInteraction?.sendRecordedMedia(false) + self.tooltipController?.dismiss() + + self.interfaceInteraction?.sendRecordedMedia(false, self.viewOnce) + } + + private weak var tooltipController: TooltipScreen? + @objc private func viewOncePressed() { + guard let context = self.context, let interfaceState = self.presentationInterfaceState else { + return + } + self.viewOnce = !self.viewOnce + + self.viewOnceButton.update(isSelected: self.viewOnce, animated: true) + + self.tooltipController?.dismiss() + if self.viewOnce { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip, hasIcon: true) + + let _ = ApplicationSpecificNotice.incrementVoiceMessagesPlayOnceSuggestion(accountManager: context.sharedContext.accountManager, count: 3).startStandalone() + } + } + + private func displayViewOnceTooltip(text: String, hasIcon: Bool) { + guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { + return + } + + let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize()) + + let tooltipController = TooltipScreen( + account: context.account, + sharedContext: context.sharedContext, + text: .markdown(text: text), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: hasIcon ? .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil) : nil, + location: .point(location, .right), + displayDuration: .default, + inset: 8.0, + cornerRadius: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + + parentController.present(tooltipController, in: .window(.root)) } @objc func waveformPressed() { @@ -353,3 +511,105 @@ private final class PlayPauseIconNode: ManagedAnimationNode { } } } + + +final class ChatRecordingViewOnceButtonNode: HighlightTrackingButtonNode { + private let backgroundNode: NavigationBackgroundNode + private let borderNode: ASImageNode + private let iconNode: ASImageNode + + private var theme: PresentationTheme? + + override init(pointerStyle: PointerStyle? = nil) { + self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.backgroundNode.isUserInteractionEnabled = false + + self.borderNode = ASImageNode() + self.borderNode.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.isUserInteractionEnabled = false + + super.init(pointerStyle: pointerStyle) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.borderNode) + self.addSubnode(self.iconNode) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateTransformScale(node: self, scale: topScale) + } else { + let transition = ContainedViewLayoutTransition.immediate + transition.updateTransformScale(node: self, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + + private var innerIsSelected = false + func update(isSelected: Bool, animated: Bool = false) { + guard let theme = self.theme else { + return + } + + let updated = self.iconNode.image == nil || self.innerIsSelected != isSelected + self.innerIsSelected = isSelected + + if animated, updated && self.iconNode.image != nil, let snapshot = self.iconNode.view.snapshotContentTree() { + self.view.addSubview(snapshot) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if updated { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceOn" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) + } + } + + func update(theme: PresentationTheme) -> CGSize { + let size = CGSize(width: 44.0, height: 44.0) + let innerSize = CGSize(width: 40.0, height: 40.0) + + if self.theme !== theme { + self.theme = theme + + self.backgroundNode.updateColor(color: theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) + + self.borderNode.image = generateCircleImage(diameter: innerSize.width, lineWidth: 0.5, color: theme.chat.historyNavigation.strokeColor, backgroundColor: nil) + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: self.innerIsSelected ? "Media Gallery/ViewOnceOn" : "Media Gallery/ViewOnce"), color: theme.chat.inputPanel.panelControlAccentColor) + } + + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - innerSize.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - innerSize.height / 2.0)), size: innerSize) + self.backgroundNode.update(size: innerSize, cornerRadius: innerSize.width / 2.0, transition: .immediate, beginWithCurrentState: false) + self.backgroundNode.frame = backgroundFrame + + if let borderImage = self.borderNode.image { + let borderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - borderImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - borderImage.size.height / 2.0)), size: borderImage.size) + self.borderNode.frame = borderFrame + } + + if let iconImage = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width / 2.0 - iconImage.size.width / 2.0), y: floorToScreenPixels(size.height / 2.0 - iconImage.size.height / 2.0)), size: iconImage.size) + self.iconNode.frame = iconFrame + } + return size + } +} diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index b3554f662a..779fad5bc5 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -562,6 +562,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator? var animatingBinNode: AnimationNode? + var viewOnce = false + let viewOnceButton: ChatRecordingViewOnceButtonNode + private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = [] private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool, Bool)? @@ -847,6 +850,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.counterTextNode = ImmediateTextNode() self.counterTextNode.textAlignment = .center + self.viewOnceButton = ChatRecordingViewOnceButtonNode() + super.init() self.viewForOverlayContent = ChatTextViewForOverlayContent( @@ -958,13 +963,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let strongSelf = self, let interfaceState = strongSelf.presentationInterfaceState, let interfaceInteraction = strongSelf.interfaceInteraction { if let _ = interfaceState.inputTextPanelState.mediaRecordingState { if sendMedia { - interfaceInteraction.finishMediaRecording(.send) + interfaceInteraction.finishMediaRecording(.send(viewOnce: strongSelf.viewOnce)) } else { interfaceInteraction.finishMediaRecording(.dismiss) } } else { interfaceInteraction.finishMediaRecording(.dismiss) } + strongSelf.viewOnce = false + strongSelf.tooltipController?.dismiss() } } self.actionButtons.micButton.offsetRecordingControls = { [weak self] in @@ -984,6 +991,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.actionButtons.micButton.stopRecording = { [weak self] in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.stopMediaRecording() + + strongSelf.tooltipController?.dismiss() } } self.actionButtons.micButton.updateLocked = { [weak self] _ in @@ -1071,6 +1080,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) } } + + self.viewOnceButton.addTarget(self, action: #selector(self.viewOncePressed), forControlEvents: [.touchUpInside]) } required init?(coder aDecoder: NSCoder) { @@ -1084,6 +1095,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.currentEmojiSuggestion?.disposable.dispose() } + override func didLoad() { + super.didLoad() + + if let viewForOverlayContent = self.viewForOverlayContent { + viewForOverlayContent.addSubnode(self.viewOnceButton) + } + } + func loadTextInputNodeIfNeeded() { if self.textInputNode == nil { self.loadTextInputNode() @@ -2001,7 +2020,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch animateCancelSlideIn = transition.isAnimated && mediaRecordingState != nil audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings, cancel: { [weak self] in + self?.viewOnce = false self?.interfaceInteraction?.finishMediaRecording(.dismiss) + self?.tooltipController?.dismiss() }) self.audioRecordingCancelIndicator = audioRecordingCancelIndicator self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0) @@ -2272,7 +2293,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch mediaRecordingAccessibilityArea.accessibilityTraits = [.button, .startsMediaSession] self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea mediaRecordingAccessibilityArea.activate = { [weak self] in - self?.interfaceInteraction?.finishMediaRecording(.send) + if let self { + self.interfaceInteraction?.finishMediaRecording(.send(viewOnce: self.viewOnce)) + } return true } self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) @@ -2510,6 +2533,16 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let prevPreviewInputPanelNode = self.prevInputPanelNode as? ChatRecordingPreviewInputPanelNode { self.prevInputPanelNode = nil + if prevPreviewInputPanelNode.viewOnceButton.alpha > 0.0 { + if let snapshotView = prevPreviewInputPanelNode.viewOnceButton.view.snapshotContentTree() { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.viewForOverlayContent?.addSubview(snapshotView) + } + } + prevPreviewInputPanelNode.gestureRecognizer?.isEnabled = false prevPreviewInputPanelNode.isUserInteractionEnabled = false @@ -2586,6 +2619,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } + + let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 44.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) + transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) + + var viewOnceIsVisible = false + if let _ = interfaceState.renderedPeer?.peer as? TelegramUser, let recordingState = interfaceState.inputTextPanelState.mediaRecordingState, case let .audio(_, isLocked) = recordingState, isLocked { + viewOnceIsVisible = true + } + if self.viewOnceButton.alpha.isZero && viewOnceIsVisible { + self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) + } + transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) + var clippingDelta: CGFloat = 0.0 if case let .media(_, _, focused) = interfaceState.inputMode, focused { clippingDelta = -panelHeight @@ -2596,6 +2645,50 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch return panelHeight } + @objc private func viewOncePressed() { + guard let interfaceState = self.presentationInterfaceState else { + return + } + self.viewOnce = !self.viewOnce + + self.viewOnceButton.update(isSelected: self.viewOnce, animated: true) + + self.tooltipController?.dismiss() + if self.viewOnce { + self.displayViewOnceTooltip(text: interfaceState.strings.Chat_PlayVoiceMessageOnceTooltip) + } + } + + private func displayViewOnceTooltip(text: String) { + guard let context = self.context, let parentController = self.interfaceInteraction?.chatController() else { + return + } + + let absoluteFrame = self.viewOnceButton.view.convert(self.viewOnceButton.bounds, to: parentController.view) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 20.0, y: absoluteFrame.midY), size: CGSize()) + + let tooltipController = TooltipScreen( + account: context.account, + sharedContext: context.sharedContext, + text: .plain(text: text), + balancedTextLayout: true, + constrainWidth: 240.0, + style: .customBlur(UIColor(rgb: 0x18181a), 0.0), + arrowStyle: .small, + icon: .animation(name: "anim_autoremove_on", delay: 0.1, tintColor: nil), + location: .point(location, .right), + displayDuration: .default, + inset: 8.0, + cornerRadius: 8.0, + shouldDismissOnTouch: { _, _ in + return .ignore + } + ) + self.tooltipController = tooltipController + + parentController.present(tooltipController, in: .window(.root)) + } + override func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool { return prevInputPanelNode is ChatRecordingPreviewInputPanelNode } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index dcb25facb6..412a55eab2 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -461,7 +461,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { var canShare = true if let (_, valueOrLoading, _) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source { switch source { - case let .telegramFile(fileReference, isCopyProtected): + case let .telegramFile(fileReference, isCopyProtected, _): canShare = !isCopyProtected strongSelf.currentFileReference = fileReference if let size = fileReference.media.size { diff --git a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift index 0dc5768525..89386c20b7 100644 --- a/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift +++ b/submodules/TelegramUI/Sources/PeerMessagesMediaPlaylist.swift @@ -63,7 +63,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem { lazy var playbackData: SharedMediaPlaybackData? = { if let file = extractFileMedia(self.message) { let fileReference = FileMediaReference.message(message: MessageReference(self.message), media: file) - let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: self.message.isCopyProtected()) + let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: self.message.isCopyProtected(), isViewOnce: self.message.minAutoremoveOrClearTimeout == viewOnceTimeout) for attribute in file.attributes { switch attribute { case let .Audio(isVoice, _, _, _, _): diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0afff5e7bc..2ed91af7e8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1603,7 +1603,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool) -> ListViewItem { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in @@ -1698,7 +1698,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { chatLocation = .peer(id: messages.first!.id.peerId) } - return ChatMessageItemImpl(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, accountPeer: accountPeer.flatMap(EnginePeer.init), forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) + return ChatMessageItemImpl(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: isPreview), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, accountPeer: accountPeer.flatMap(EnginePeer.init), forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 77ee1a4af1..a5961e2876 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -229,13 +229,13 @@ final class SharedMediaPlayer { switch playbackData.type { case .voice, .music: switch playbackData.source { - case let .telegramFile(fileReference, _): + case let .telegramFile(fileReference, _, _): strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity, isAudioVideoMessage: playbackData.type == .voice)) } case .instantVideo: if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { - case let .telegramFile(fileReference, _): + case let .telegramFile(fileReference, _, _): let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) @@ -493,7 +493,7 @@ final class SharedMediaPlayer { let fetchedCurrentSignal: Signal let fetchedNextSignal: Signal switch current { - case let .telegramFile(file, _): + case let .telegramFile(file, _, _): fetchedCurrentSignal = self.account.postbox.mediaBox.resourceData(file.media.resource) |> mapToSignal { data -> Signal in if data.complete { @@ -506,7 +506,7 @@ final class SharedMediaPlayer { |> ignoreValues } switch next { - case let .telegramFile(file, _): + case let .telegramFile(file, _, _): fetchedNextSignal = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, userLocation: .other, userContentType: .audio, reference: file.resourceReference(file.media.resource)) |> ignoreValues |> `catch` { _ -> Signal in