From 1f285271b2582241c5b5778ff58bd2be2797b379 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 17 Sep 2021 21:25:13 +0300 Subject: [PATCH] Various Improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 29 ++++++ submodules/ContactListUI/BUILD | 2 + .../Sources/ContactsSearchContainerNode.swift | 89 ++++++++++++++++--- .../ChatItemGalleryFooterContentNode.swift | 14 ++- .../Sources/ShareController.swift | 2 +- .../Sources/ShareControllerNode.swift | 86 ++++++++++-------- .../Sources/VoiceChatController.swift | 54 ++++++++--- .../VoiceChatRecordingSetupController.swift | 31 ++++--- .../Sources/VoiceChatTimerNode.swift | 34 +++++++ .../TelegramNotices/Sources/Notices.swift | 30 +++++++ .../TelegramUI/Sources/ChatController.swift | 45 +++++++++- .../Sources/ChatControllerInteraction.swift | 4 + .../ChatInterfaceInputContextPanels.swift | 2 +- .../ChatInterfaceStateContextMenus.swift | 2 +- .../ChatMessageAnimatedStickerItemNode.swift | 14 ++- .../ChatRecentActionsControllerNode.swift | 1 + .../Sources/DrawingStickersScreen.swift | 1 + .../Sources/InlineReactionSearchPanel.swift | 53 ++++++++--- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/PeerInfo/PeerInfoScreen.swift | 1 + .../Sources/PeerSelectionControllerNode.swift | 2 +- .../Sources/SharedAccountContext.swift | 1 + 22 files changed, 407 insertions(+), 91 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 63fa70f5c4..a896f1b90f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6622,7 +6622,9 @@ Sorry for the inconvenience."; "VoiceChat.VideoPreviewShareScreenInfo" = "Everything on your screen\nwill be shared"; "Gallery.SaveToGallery" = "Save to Gallery"; +"Gallery.ImageSaved" = "Image Saved"; "Gallery.VideoSaved" = "Video Saved"; +"Gallery.ImagesAndVideosSaved" = "Media Saved"; "Gallery.WaitForVideoDownoad" = "Please wait for the video to be fully downloaded."; "Gallery.SaveImage" = "Save Image"; @@ -6832,3 +6834,30 @@ Ads should no longer be synonymous with abuse of user privacy. Let us redefine h "CHAT_MESSAGE_NOTHEME" = "%1$@ set theme to default one in the group %2$@"; "Activity.EnjoyingAnimations" = "enjoying %@ animations"; + +"Conversation.InteractiveEmojiSyncTip" = "If %@ was viewing the chat right now, he would also enjoy this animation."; + +"Contacts.Search.NoResults" = "No Results"; +"Contacts.Search.NoResultsQueryDescription" = "There were no results for \"%@\".\nTry a new search."; + +"LiveStream.Listening.Members_0" = "%@ listening"; +"LiveStream.Listening.Members_1" = "%@ listening"; +"LiveStream.Listening.Members_2" = "%@ listening"; +"LiveStream.Listening.Members_3_10" = "%@ listening"; +"LiveStream.Listening.Members_many" = "%@ listening"; +"LiveStream.Listening.Members_any" = "%@ listening"; + +"LiveStream.Watching.Members_0" = "%@ watching"; +"LiveStream.Watching.Members_1" = "%@ watching"; +"LiveStream.Watching.Members_2" = "%@ watching"; +"LiveStream.Watching.Members_3_10" = "%@ watching"; +"LiveStream.Watching.Members_many" = "%@ watching"; +"LiveStream.Watching.Members_any" = "%@ watching"; + +"VoiceChat.RecordTitle" = "Record Video Chat"; +"LiveStream.RecordTitle" = "Record Live Stream"; +"VoiceChat.RecordVideoAndAudio" = "Video and Audio"; +"VoiceChat.RecordOnlyAudio" = "Only Audio"; +"VoiceChat.RecordPortrait" = "Portrait"; +"VoiceChat.RecordLandscape" = "Landscape"; +"VoiceChat.RecordStartRecording" = "Start Recording"; diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index aa31a500f9..e4748b0bc4 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -34,6 +34,8 @@ swift_library( "//submodules/PhoneNumberFormat:PhoneNumberFormat", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/StickerResources:StickerResources", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index b87f51f053..2cd4300906 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -15,6 +15,8 @@ import ContactsPeerItem import ContextUI import PhoneNumberFormat import ItemListUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode private enum ContactListSearchGroup { case contacts @@ -175,16 +177,18 @@ struct ContactListSearchContainerTransition { let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let isSearching: Bool + let emptyResults: Bool + let query: String } -private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ContactListSearchContainerTransition { +private func contactListSearchContainerPreparedRecentTransition(from fromEntries: [ContactListSearchEntry], to toEntries: [ContactListSearchEntry], isSearching: Bool, emptyResults: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) -> ContactListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, addContact: addContact, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, timeFormat: timeFormat, addContact: addContact, openPeer: openPeer, contextAction: contextAction), directionHint: nil) } - return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) + return ContactListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, emptyResults: emptyResults, query: query) } public struct ContactsSearchCategories: OptionSet { @@ -208,6 +212,11 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo private let dimNode: ASDisplayNode public let listNode: ListView + private let emptyResultsTitleNode: ImmediateTextNode + private let emptyResultsTextNode: ImmediateTextNode + private let emptyResultsAnimationNode: AnimatedStickerNode + private var emptyResultsAnimationSize: CGSize = CGSize() + private let searchQuery = Promise() private let searchDisposable = MetaDisposable() @@ -241,14 +250,36 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } + self.emptyResultsTitleNode = ImmediateTextNode() + self.emptyResultsTitleNode.displaysAsynchronously = false + self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Contacts_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) + self.emptyResultsTitleNode.textAlignment = .center + self.emptyResultsTitleNode.isHidden = true + + self.emptyResultsTextNode = ImmediateTextNode() + self.emptyResultsTextNode.displaysAsynchronously = false + self.emptyResultsTextNode.maximumNumberOfLines = 0 + self.emptyResultsTextNode.textAlignment = .center + self.emptyResultsTextNode.isHidden = true + + self.emptyResultsAnimationNode = AnimatedStickerNode() + self.emptyResultsAnimationNode.isHidden = true + super.init() + self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + self.emptyResultsAnimationSize = CGSize(width: 148.0, height: 148.0) + self.backgroundColor = nil self.isOpaque = false self.addSubnode(self.dimNode) self.addSubnode(self.listNode) + self.addSubnode(self.emptyResultsAnimationNode) + self.addSubnode(self.emptyResultsTitleNode) + self.addSubnode(self.emptyResultsTextNode) + self.listNode.isHidden = true let themeAndStringsPromise = self.themeAndStringsPromise @@ -256,7 +287,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo let previousFoundRemoteContacts = Atomic<([FoundPeer], [FoundPeer])?>(value: nil) let searchItems = self.searchQuery.get() - |> mapToSignal { query -> Signal<[ContactListSearchEntry]?, NoError> in + |> mapToSignal { query -> Signal<([ContactListSearchEntry]?, String), NoError> in if let query = query, !query.isEmpty { let foundLocalContacts: Signal<([Peer], [PeerId: PeerPresence]), NoError> if categories.contains(.cloudContacts) { @@ -286,7 +317,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo return combineLatest(foundLocalContacts, foundRemoteContacts, foundDeviceContacts, themeAndStringsPromise.get()) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) - |> map { localPeersAndPresences, remotePeers, deviceContacts, themeAndStrings -> [ContactListSearchEntry] in + |> map { localPeersAndPresences, remotePeers, deviceContacts, themeAndStrings -> ([ContactListSearchEntry], String) in let _ = previousFoundRemoteContacts.swap(remotePeers) var entries: [ContactListSearchEntry] = [] @@ -381,18 +412,18 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo entries.append(.addContact(themeAndStrings.0, themeAndStrings.1, query)) } - return entries + return (entries, query) } } else { let _ = previousFoundRemoteContacts.swap(nil) - return .single(nil) + return .single((nil, "")) } } let previousSearchItems = Atomic<[ContactListSearchEntry]>(value: []) self.searchDisposable.set((searchItems - |> deliverOnMainQueue).start(next: { [weak self] items in + |> deliverOnMainQueue).start(next: { [weak self] items, query in if let strongSelf = self { let previousItems = previousSearchItems.swap(items ?? []) @@ -404,7 +435,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo } } - let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, addContact: addContact, openPeer: { peer in + let transition = contactListSearchContainerPreparedRecentTransition(from: previousItems, to: items ?? [], isSearching: items != nil, emptyResults: items?.isEmpty ?? false, query: query, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, timeFormat: strongSelf.presentationData.dateTimeFormat, addContact: addContact, openPeer: { peer in self?.listNode.clearHighlightAnimated(true) self?.openPeer(peer) }, contextAction: strongSelf.contextAction) @@ -462,6 +493,27 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + let size = layout.size + let sideInset = layout.safeInsets.left + let visibleHeight = layout.size.height + let bottomInset = layout.insets(options: .input).bottom + + let padding: CGFloat = 16.0 + let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let emptyAnimationHeight = self.emptyResultsAnimationSize.height + let emptyAnimationSpacing: CGFloat = 8.0 + let emptyTextSpacing: CGFloat = 8.0 + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + + let textTransition = ContainedViewLayoutTransition.immediate + textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.emptyResultsAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyResultsAnimationSize)) + textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) + textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + self.emptyResultsAnimationNode.updateLayout(size: self.emptyResultsAnimationSize) + if !hadValidLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() @@ -488,9 +540,26 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo options.insert(.PreferSynchronousResourceLoading) let isSearching = transition.isSearching + let emptyResults = transition.emptyResults + let query = transition.query self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in - self?.listNode.isHidden = !isSearching - self?.dimNode.isHidden = isSearching + guard let strongSelf = self else { + return + } + + strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Contacts_Search_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) + + if let (layout, navigationBarHeight) = strongSelf.containerViewLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + strongSelf.listNode.isHidden = !isSearching + strongSelf.dimNode.isHidden = isSearching + + strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults + strongSelf.emptyResultsTitleNode.isHidden = !emptyResults + strongSelf.emptyResultsTextNode.isHidden = !emptyResults + strongSelf.emptyResultsAnimationNode.visibility = emptyResults + }) } } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 7a260ecc23..208f1af5e9 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -1114,15 +1114,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } var preferredAction = ShareControllerPreferredAction.default + var actionCompletionText: String = "" if let generalMessageContentKind = generalMessageContentKind { switch generalMessageContentKind { - case .image, .video: + case .image: preferredAction = .saveToCameraRoll + actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved + case .video: + preferredAction = .saveToCameraRoll + actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved default: break } } else if messageContentKinds.count == 2 && messageContentKinds.contains(.image) && messageContentKinds.contains(.video) { preferredAction = .saveToCameraRoll + actionCompletionText = strongSelf.presentationData.strings.Gallery_ImagesAndVideosSaved } if messages.count == 1 { @@ -1178,6 +1184,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll shareController.dismissed = { [weak self] _ in self?.interacting?(false) } + shareController.actionCompleted = { [weak self] in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + } shareController.completed = { [weak self] peerIds in if let strongSelf = self { let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 04e456cfdd..e917287c86 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -882,7 +882,7 @@ public final class ShareController: ViewController { } else { context = self.sharedContext.makeTempAccountContext(account: self.currentAccount) } - self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: .standalone(media: media)) |> map(Optional.init)) + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true) } private func saveToCameraRoll(mediaReference: AnyMediaReference) { diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 46ce491d0e..142255a79e 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -925,46 +925,60 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate })) } - func transitionToProgressWithValue(signal: Signal) { + func transitionToProgressWithValue(signal: Signal, dismissImmediately: Bool = false) { self.inputFieldNode.deactivateInput() - let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) - transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) - transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) - transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) - transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) - self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: true), fastOut: true) - - let timestamp = CACurrentMediaTime() - var wasDone = false - let doneImpl: (Bool) -> Void = { [weak self] shouldDelay in - let minDelay: Double = shouldDelay ? 0.9 : 0.6 - let delay = max(minDelay, (timestamp + minDelay) - CACurrentMediaTime()) - Queue.mainQueue().after(delay, { + if dismissImmediately { + self.animateOut(shared: true, completion: {}) + + self.shareDisposable.set((signal + |> deliverOnMainQueue).start(next: { _ in + + }, completed: { [weak self] in if let strongSelf = self { - strongSelf.animateOut(shared: true, completion: { - self?.dismiss?(true) - }) + strongSelf.dismiss?(true) } - }) + })) + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.12, curve: .easeInOut) + transition.updateAlpha(node: self.actionButtonNode, alpha: 0.0) + transition.updateAlpha(node: self.inputFieldNode, alpha: 0.0) + transition.updateAlpha(node: self.actionSeparatorNode, alpha: 0.0) + transition.updateAlpha(node: self.actionsBackgroundNode, alpha: 0.0) + + self.transitionToContentNode(ShareLoadingContainerNode(theme: self.presentationData.theme, forceNativeAppearance: true), fastOut: true) + + let timestamp = CACurrentMediaTime() + var wasDone = false + let doneImpl: (Bool) -> Void = { [weak self] shouldDelay in + let minDelay: Double = shouldDelay ? 0.9 : 0.6 + let delay = max(minDelay, (timestamp + minDelay) - CACurrentMediaTime()) + Queue.mainQueue().after(delay, { + if let strongSelf = self { + strongSelf.animateOut(shared: true, completion: { + self?.dismiss?(true) + }) + } + }) + } + self.shareDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self, let contentNode = strongSelf.contentNode as? ShareLoadingContainerNode else { + return + } + if let status = status { + contentNode.state = .progress(status) + } + }, completed: { [weak self] in + guard let strongSelf = self, let contentNode = strongSelf.contentNode as? ShareLoadingContainerNode else { + return + } + contentNode.state = .done + if !wasDone { + wasDone = true + doneImpl(true) + } + })) } - self.shareDisposable.set((signal - |> deliverOnMainQueue).start(next: { [weak self] status in - guard let strongSelf = self, let contentNode = strongSelf.contentNode as? ShareLoadingContainerNode else { - return - } - if let status = status { - contentNode.state = .progress(status) - } - }, completed: { [weak self] in - guard let strongSelf = self, let contentNode = strongSelf.contentNode as? ShareLoadingContainerNode else { - return - } - contentNode.state = .done - if !wasDone { - wasDone = true - doneImpl(true) - } - })) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 63022e55da..f2a5171029 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -802,6 +802,7 @@ public final class VoiceChatController: ViewController { private var scheduleButtonTitle = "" private let titleNode: VoiceChatTitleNode + private let participantsNode: VoiceChatTimerNode private var enqueuedTransitions: [ListTransition] = [] private var enqueuedFullscreenTransitions: [ListTransition] = [] @@ -828,6 +829,7 @@ public final class VoiceChatController: ViewController { private var currentSubtitle: String = "" private var currentSpeakingSubtitle: String? private var currentCallMembers: ([GroupCallParticipantsContext.Participant], String?)? + private var currentTotalCount: Int32 = 0 private var currentInvitedPeers: [Peer]? private var currentSpeakingPeers: Set? private var currentContentOffset: CGFloat? @@ -1132,6 +1134,8 @@ public final class VoiceChatController: ViewController { self.timerNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) self.timerNode.isHidden = true + self.participantsNode = VoiceChatTimerNode(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) + super.init() let context = self.context @@ -1847,6 +1851,7 @@ public final class VoiceChatController: ViewController { self.listContainer.addSubnode(self.topCornersNode) self.contentContainer.addSubnode(self.bottomGradientNode) self.contentContainer.addSubnode(self.bottomPanelBackgroundNode) + self.contentContainer.addSubnode(self.participantsNode) self.contentContainer.addSubnode(self.tileGridNode) self.contentContainer.addSubnode(self.mainStageContainerNode) self.contentContainer.addSubnode(self.transitionContainerNode) @@ -1935,7 +1940,10 @@ public final class VoiceChatController: ViewController { strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: (callMembers?.participants ?? [], callMembers?.loadMoreToken), invitedPeers: invitedPeers, speakingPeers: callMembers?.speakingParticipants ?? []) - let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0))) + let totalCount = Int32(max(1, callMembers?.totalCount ?? 0)) + strongSelf.currentTotalCount = totalCount + + let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(totalCount) strongSelf.currentSubtitle = subtitle if strongSelf.isScheduling { @@ -1979,6 +1987,14 @@ public final class VoiceChatController: ViewController { let (title, isRecording) = titleAndRecording if let peer = peerViewMainPeer(view) { + let isLivestream: Bool + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + isLivestream = true + } else { + isLivestream = false + } + strongSelf.participantsNode.isHidden = !isLivestream + strongSelf.peer = peer strongSelf.currentTitleIsCustom = title != nil strongSelf.currentTitle = title ?? peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) @@ -2211,12 +2227,10 @@ public final class VoiceChatController: ViewController { return } if event.joined { - let text: String if let channel = strongSelf.peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.LiveStream_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } else { - text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string + return } + let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: text), action: { _ in return false }) } })) @@ -2651,11 +2665,11 @@ public final class VoiceChatController: ViewController { }, action: { _, f in f(.dismissWithoutContent) - guard let strongSelf = self else { + guard let strongSelf = self, let peer = strongSelf.peer else { return } - let controller = VoiceChatRecordingSetupController(context: strongSelf.context, completion: { [weak self] videoOrientation in + let controller = VoiceChatRecordingSetupController(context: strongSelf.context, peer: peer, completion: { [weak self] videoOrientation in if let strongSelf = self { let title: String let text: String @@ -3958,6 +3972,10 @@ public final class VoiceChatController: ViewController { transition.animatePositionAdditive(node: self.bottomPanelBackgroundNode, offset: positionDelta) } } + + let participantsFrame = CGRect(x: 0.0, y: bottomCornersFrame.maxY - 100.0, width: size.width, height: 216.0) + transition.updateFrame(node: self.participantsNode, frame: participantsFrame) + self.participantsNode.update(size: participantsFrame.size, participants: self.currentTotalCount, groupingSeparator: self.presentationData.dateTimeFormat.groupingSeparator, transition: .immediate) } private var decorationsAreDark: Bool? @@ -4455,7 +4473,7 @@ public final class VoiceChatController: ViewController { let timerFrame = CGRect(x: 0.0, y: layout.size.height - bottomPanelHeight - 216.0, width: size.width, height: 216.0) transition.updateFrame(node: self.timerNode, frame: timerFrame) self.timerNode.update(size: timerFrame.size, scheduleTime: self.callState?.scheduleTimestamp, transition: .immediate) - + let scheduleTextSize = self.scheduleTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) self.scheduleTextNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scheduleTextSize.width) / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - scheduleTextSize.height - 145.0), size: scheduleTextSize) @@ -5002,6 +5020,15 @@ public final class VoiceChatController: ViewController { displayPanelVideos = self.displayPanelVideos } + let isLivestream: Bool + if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { + isLivestream = true + } else { + isLivestream = false + } + + let canManageCall = self.callState?.canManageCall ?? false + var joinedVideo = self.joinedVideo ?? true var myEntry: VoiceChatPeerEntry? @@ -5014,7 +5041,10 @@ public final class VoiceChatController: ViewController { let memberState: VoiceChatPeerEntry.State var memberMuteState: GroupCallParticipantsContext.Participant.MuteState? - if member.hasRaiseHand && !(member.muteState?.canUnmute ?? false) { + if member.hasRaiseHand && !(member.muteState?.canUnmute ?? true) { + if isLivestream && !canManageCall { + continue + } memberState = .raisedHand memberMuteState = member.muteState @@ -5050,6 +5080,10 @@ public final class VoiceChatController: ViewController { disposable.dispose() self.raisedHandDisplayDisposables[member.peer.id] = nil } + + if isLivestream && !(memberMuteState?.canUnmute ?? true) { + continue + } } var memberPeer = member.peer @@ -5080,7 +5114,7 @@ public final class VoiceChatController: ViewController { effectiveSpeakerVideoEndpointId: self.effectiveSpeaker?.1, state: memberState, muteState: memberMuteState, - canManageCall: self.callState?.canManageCall ?? false, + canManageCall: canManageCall, volume: member.volume, raisedHand: member.hasRaiseHand, displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id), diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift index 0c307e7d9d..7aa79f1a7b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift @@ -18,14 +18,16 @@ final class VoiceChatRecordingSetupController: ViewController { } private let context: AccountContext + private let peer: Peer private let completion: (Bool?) -> Void private var animatedIn = false private var presentationDataDisposable: Disposable? - init(context: AccountContext, completion: @escaping (Bool?) -> Void) { + init(context: AccountContext, peer: Peer, completion: @escaping (Bool?) -> Void) { self.context = context + self.peer = peer self.completion = completion super.init(navigationBarPresentationData: nil) @@ -53,7 +55,7 @@ final class VoiceChatRecordingSetupController: ViewController { } override public func loadDisplayNode() { - self.displayNode = VoiceChatRecordingSetupControllerNode(controller: self, context: self.context) + self.displayNode = VoiceChatRecordingSetupControllerNode(controller: self, context: self.context, peer: self.peer) self.controllerNode.completion = { [weak self] videoOrientation in self?.completion(videoOrientation) } @@ -147,7 +149,7 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, var dismiss: (() -> Void)? var cancel: (() -> Void)? - init(controller: VoiceChatRecordingSetupController, context: AccountContext) { + init(controller: VoiceChatRecordingSetupController, context: AccountContext, peer: Peer) { self.controller = controller self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -180,7 +182,14 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, self.contentBackgroundNode = ASDisplayNode() self.contentBackgroundNode.backgroundColor = backgroundColor - let title = "Record Voice Chat" + let isLivestream: Bool + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + isLivestream = true + } else { + isLivestream = false + } + + let title = isLivestream ? self.presentationData.strings.LiveStream_RecordTitle : self.presentationData.strings.VoiceChat_RecordTitle self.titleNode = ASTextNode() self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor) @@ -200,14 +209,14 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, self.videoAudioButton = HighlightTrackingButtonNode() self.videoAudioTitleNode = ImmediateTextNode() - self.videoAudioTitleNode.attributedText = NSAttributedString(string: "Video and Audio", font: Font.regular(17.0), textColor: .white, paragraphAlignment: .left) + self.videoAudioTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.VoiceChat_RecordVideoAndAudio, font: Font.regular(17.0), textColor: .white, paragraphAlignment: .left) self.videoAudioCheckNode = ASImageNode() self.videoAudioCheckNode.displaysAsynchronously = false self.videoAudioCheckNode.image = UIImage(bundleImageName: "Call/Check") self.audioButton = HighlightTrackingButtonNode() self.audioTitleNode = ImmediateTextNode() - self.audioTitleNode.attributedText = NSAttributedString(string: "Only Audio", font: Font.regular(17.0), textColor: .white, paragraphAlignment: .left) + self.audioTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.VoiceChat_RecordOnlyAudio, font: Font.regular(17.0), textColor: .white, paragraphAlignment: .left) self.audioCheckNode = ASImageNode() self.audioCheckNode.displaysAsynchronously = false self.audioCheckNode.image = UIImage(bundleImageName: "Call/Check") @@ -217,14 +226,14 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, self.portraitButton.cornerRadius = 11.0 self.portraitIconNode = PreviewIconNode() self.portraitTitleNode = ImmediateTextNode() - self.portraitTitleNode.attributedText = NSAttributedString(string: "Portrait", font: Font.semibold(15.0), textColor: UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + self.portraitTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.VoiceChat_RecordPortrait, font: Font.semibold(15.0), textColor: UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) self.landscapeButton = HighlightTrackingButtonNode() self.landscapeButton.backgroundColor = UIColor(rgb: 0x303032) self.landscapeButton.cornerRadius = 11.0 self.landscapeIconNode = PreviewIconNode() self.landscapeTitleNode = ImmediateTextNode() - self.landscapeTitleNode.attributedText = NSAttributedString(string: "Landscape", font: Font.semibold(15.0), textColor: UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + self.landscapeTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.VoiceChat_RecordLandscape, font: Font.semibold(15.0), textColor: UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) self.selectionNode = ASImageNode() self.selectionNode.displaysAsynchronously = false @@ -515,8 +524,8 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, transition.updateAlpha(node: self.selectionNode, alpha: buttonsAlpha) - self.portraitTitleNode.attributedText = NSAttributedString(string: "Portrait", font: Font.semibold(15.0), textColor: self.videoMode == .portrait ? UIColor(rgb: 0xb56df4) : UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) - self.landscapeTitleNode.attributedText = NSAttributedString(string: "Landscape", font: Font.semibold(15.0), textColor: self.videoMode == .landscape ? UIColor(rgb: 0xb56df4) : UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + self.portraitTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.VoiceChat_RecordPortrait, font: Font.semibold(15.0), textColor: self.videoMode == .portrait ? UIColor(rgb: 0xb56df4) : UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) + self.landscapeTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.VoiceChat_RecordLandscape, font: Font.semibold(15.0), textColor: self.videoMode == .landscape ? UIColor(rgb: 0xb56df4) : UIColor(rgb: 0x8e8e93), paragraphAlignment: .left) let buttonWidth = floorToScreenPixels((contentFrame.width - inset * 2.0 - 11.0) / 2.0) let portraitButtonFrame = CGRect(x: inset, y: 56.0 + itemHeight * 2.0 + 25.0, width: buttonWidth, height: 140.0) @@ -547,7 +556,7 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, self.selectionNode.frame = landscapeButtonFrame.insetBy(dx: -1.0, dy: -1.0) } - self.doneButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: .button(text: "Start Recording"), title: "", subtitle: "", dark: false, small: false) + self.doneButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: .button(text: self.presentationData.strings.VoiceChat_RecordStartRecording), title: "", subtitle: "", dark: false, small: false) let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: cancelButtonHeight)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift index 56f3ae3a9a..3fb3b29b7d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTimerNode.swift @@ -119,6 +119,40 @@ final class VoiceChatTimerNode: ASDisplayNode { } } + func update(size: CGSize, participants: Int32, groupingSeparator: String, transition: ContainedViewLayoutTransition) { + if self.validLayout == nil { + self.updateAnimations() + } + self.validLayout = size + + self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + self.foregroundGradientLayer.frame = self.foregroundView.bounds + self.maskView.frame = self.foregroundView.bounds + + let text: String = presentationStringsFormattedNumber(participants, groupingSeparator) + let subtitle = "listening" + + self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white) + let titleSize = self.titleNode.updateLayout(size) + self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height) + + self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) + + var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) + if timerSize.width > size.width - 32.0 { + self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) + timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) + } + + self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height) + + self.subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 21.0, design: .round, weight: .semibold, traits: []), textColor: .white) + let subtitleSize = self.subtitleNode.updateLayout(size) + self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height) + + self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + } + func update(size: CGSize, scheduleTime: Int32?, transition: ContainedViewLayoutTransition) { if self.validLayout == nil { self.updateAnimations() diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index d45d82d04b..c186837a63 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -202,6 +202,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case messageViewsPrivacyTips = 25 case chatSpecificThemeLightPreviewTip = 26 case chatSpecificThemeDarkPreviewTip = 27 + case interactiveEmojiSyncTip = 28 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -357,6 +358,10 @@ private struct ApplicationSpecificNoticeKeys { static func chatForwardOptionsTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatForwardOptionsTip.key) } + + static func interactiveEmojiSyncTip() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.interactiveEmojiSyncTip.key) + } } public struct ApplicationSpecificNotice { @@ -961,6 +966,31 @@ public struct ApplicationSpecificNotice { } } + public static func getInteractiveEmojiSyncTip(accountManager: AccountManager) -> Signal<(Int32, Int32), NoError> { + return accountManager.transaction { transaction -> (Int32, Int32) in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.interactiveEmojiSyncTip()) as? ApplicationSpecificTimestampAndCounterNotice { + return (value.counter, value.timestamp) + } else { + return (0, 0) + } + } + } + + public static func incrementInteractiveEmojiSyncTip(accountManager: AccountManager, count: Int = 1, timestamp: Int32) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.interactiveEmojiSyncTip()) as? ApplicationSpecificTimestampAndCounterNotice { + currentValue = value.counter + } + let previousValue = currentValue + currentValue += Int32(count) + + transaction.setNotice(ApplicationSpecificNoticeKeys.interactiveEmojiSyncTip(), ApplicationSpecificTimestampAndCounterNotice(counter: currentValue, timestamp: timestamp)) + + return Int(previousValue) + } + } + public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 051989afe3..60859496ea 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -329,6 +329,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var chatUnreadMentionCountDisposable: Disposable? private var peerInputActivitiesDisposable: Disposable? + private var peerInputActivitiesPromise = Promise<[(Peer, PeerInputActivity)]>() + private var interactiveEmojiSyncDisposable = MetaDisposable() + private var recentlyUsedInlineBotsValue: [Peer] = [] private var recentlyUsedInlineBotsDisposable: Disposable? @@ -2883,6 +2886,43 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.choosingStickerActivityPromise.set(value) } + }, commitEmojiInteraction: { [weak self] messageId, emoji, interaction, file in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer else { + return + } + strongSelf.context.account.updateLocalInputActivity(peerId: PeerActivitySpace(peerId: messageId.peerId, category: .global), activity: .interactingWithEmoji(emoticon: emoji, messageId: messageId, interaction: interaction), isPresent: true) + + let currentTimestamp = Int32(Date().timeIntervalSince1970) + let _ = (ApplicationSpecificNotice.getInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count, timestamp in + if let strongSelf = self, count < 3 && currentTimestamp > timestamp + 24 * 60 * 60 { + strongSelf.interactiveEmojiSyncDisposable.set( + (strongSelf.peerInputActivitiesPromise.get() + |> filter { activities -> Bool in + var found = false + for (_, activity) in activities { + if case .seeingEmojiInteraction(emoji) = activity { + found = true + break + } + } + return found + } + |> map { _ -> Bool in + return true + } + |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(false))).start(next: { [weak self] responded in + if let strongSelf = self { + if !responded { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(peer.compactDisplayTitle).string), elevatedLayout: false, action: { _ in return false }), in: .current) + + let _ = ApplicationSpecificNotice.incrementInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start() + } + } + }) + ) + } + }) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -4196,6 +4236,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.buttonUnreadCountDisposable?.dispose() self.chatUnreadMentionCountDisposable?.dispose() self.peerInputActivitiesDisposable?.dispose() + self.interactiveEmojiSyncDisposable.dispose() self.recentlyUsedInlineBotsDisposable?.dispose() self.unpinMessageDisposable?.dispose() self.inputActivityDisposable?.dispose() @@ -5481,7 +5522,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G dismissAction() strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in let _ = (strongSelf.context.engine.peers.reportPeerMessages(messageIds: Array(messageIds), reason: reportReason, message: message) - |> deliverOnMainQueue).start(completed: { [weak self] in + |> deliverOnMainQueue).start(completed: { strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) }) }) @@ -7454,6 +7495,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) strongSelf.chatTitleView?.inputActivities = (peerId, displayActivities) + strongSelf.peerInputActivitiesPromise.set(.single(activities)) + for activity in activities { if case let .interactingWithEmoji(emoticon, messageId, maybeInteraction) = activity.1, let interaction = maybeInteraction { var found = false diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index a2367f0306..d5836f9602 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -125,6 +125,7 @@ public final class ChatControllerInteraction { let isAnimatingMessage: (UInt32) -> Bool let getMessageTransitionNode: () -> ChatMessageTransitionNode? let updateChoosingSticker: (Bool) -> Void + let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -221,6 +222,7 @@ public final class ChatControllerInteraction { isAnimatingMessage: @escaping (UInt32) -> Bool, getMessageTransitionNode: @escaping () -> ChatMessageTransitionNode?, updateChoosingSticker: @escaping (Bool) -> Void, + commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, @@ -303,6 +305,7 @@ public final class ChatControllerInteraction { self.isAnimatingMessage = isAnimatingMessage self.getMessageTransitionNode = getMessageTransitionNode self.updateChoosingSticker = updateChoosingSticker + self.commitEmojiInteraction = commitEmojiInteraction self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -362,6 +365,7 @@ public final class ChatControllerInteraction { }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 3a2da7ab2a..fb50e69ca7 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -85,7 +85,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa currentPanel.updateResults(results: results.map({ $0.file }), query: query) return currentPanel } else { - let panel = InlineReactionSearchPanel(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize) + let panel = InlineReactionSearchPanel(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, peerId: chatPresentationInterfaceState.renderedPeer?.peerId) panel.controllerInteraction = controllerInteraction panel.interfaceInteraction = interfaceInteraction panel.updateResults(results: results.map({ $0.file }), query: query) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 79de796ca3..cea2b4fcbd 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -783,7 +783,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: mediaReference) |> deliverOnMainQueue).start(completed: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controllerInteraction.presentGlobalOverlayController(OverlayStatusController(theme: presentationData.theme, type: .success), nil) + controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: isVideo ? presentationData.strings.Gallery_VideoSaved : presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }) f(.default) }))) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 5432dd0d4d..9770b48c1c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -526,14 +526,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let textEmoji = item.message.text.strippedEmoji var additionalTextEmoji = textEmoji let (basicEmoji, fitz) = item.message.text.basicEmoji - if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค"].contains(textEmoji) { + if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค", "๐ŸคŽ", "๐Ÿค"].contains(textEmoji) { additionalTextEmoji = "โค๏ธ".strippedEmoji } else if fitz != nil { additionalTextEmoji = basicEmoji } var animationItems: [Int: StickerPackItem]? - if let items = item.associatedData.additionalAnimatedEmojiStickers[item.message.text.strippedEmoji] { + if let items = item.associatedData.additionalAnimatedEmojiStickers[textEmoji] { animationItems = items } else if let items = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji] { animationItems = items @@ -1349,10 +1349,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } private func commitEnqueuedAnimations() { - guard let item = self.item, !self.enqueuedAdditionalAnimations.isEmpty else { + guard let item = self.item, let file = self.emojiFile, !self.enqueuedAdditionalAnimations.isEmpty else { return } - let textEmoji = item.message.text.strippedEmoji let enqueuedAnimations = self.enqueuedAdditionalAnimations self.enqueuedAdditionalAnimations.removeAll() @@ -1365,8 +1364,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for (index, timestamp) in enqueuedAnimations { animations.append(EmojiInteraction.Animation(index: index, timeOffset: Float(max(0.0, timestamp - startTimestamp)))) } - - item.context.account.updateLocalInputActivity(peerId: PeerActivitySpace(peerId: item.message.id.peerId, category: .global), activity: .interactingWithEmoji(emoticon: textEmoji, messageId: item.message.id, interaction: EmojiInteraction(animations: animations)), isPresent: true) + item.controllerInteraction.commitEmojiInteraction(item.message.id, item.message.text.strippedEmoji, EmojiInteraction(animations: animations), file) } func playEmojiInteraction(_ interaction: EmojiInteraction) { @@ -1423,7 +1421,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let textEmoji = item.message.text.strippedEmoji var additionalTextEmoji = textEmoji let (basicEmoji, fitz) = item.message.text.basicEmoji - if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค"].contains(textEmoji) { + if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค", "๐ŸคŽ", "๐Ÿค"].contains(textEmoji) { additionalTextEmoji = "โค๏ธ".strippedEmoji } else if fitz != nil { additionalTextEmoji = basicEmoji @@ -1595,7 +1593,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let (basicEmoji, fitz) = text.basicEmoji - if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค", "โค๏ธ"].contains(textEmoji) { + if ["๐Ÿ’›", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’œ", "๐Ÿงก", "๐Ÿ–ค", "๐ŸคŽ", "๐Ÿค", "โค๏ธ"].contains(textEmoji) { additionalTextEmoji = "โค๏ธ".strippedEmoji } else if fitz != nil { additionalTextEmoji = basicEmoji diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 233037527b..39c5dd6072 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -530,6 +530,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 1c37995328..663de3787c 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -156,6 +156,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 8695118b76..ea05c3b26c 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -25,6 +25,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie private let context: AccountContext private var theme: PresentationTheme private var strings: PresentationStrings + private let peerId: PeerId? private let scrollNode: ASScrollNode private var items: [TelegramMediaFile] = [] @@ -36,6 +37,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie private var ignoreScrolling: Bool = false private var animateInOnLayout: Bool = false + private weak var peekController: PeekController? + var previewedStickerItem: StickerPackItem? var updateBackgroundOffset: ((CGFloat, Bool, ContainedViewLayoutTransition) -> Void)? @@ -53,10 +56,11 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie |> distinctUntilChanged } - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peerId: PeerId?) { self.context = context self.theme = theme self.strings = strings + self.peerId = peerId self.scrollNode = ASScrollNode() @@ -99,12 +103,36 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { var menuItems: [ContextMenuItem] = [] - menuItems = [ - .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in + + if strongSelf.peerId != strongSelf.context.account.peerId && strongSelf.peerId?.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + } + } + f(.default) + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + } + } f(.default) - - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) - })), + }))) + + menuItems.append( .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -115,7 +143,10 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } - })), + })) + ) + + menuItems.append( .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -141,7 +172,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie } } } - }))] + })) + ) return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { return nil @@ -159,6 +191,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie controller.visibilityUpdated = { [weak self] visible in self?.previewingStickersPromise.set(visible) } + strongSelf.peekController = controller strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(controller, nil) return controller } @@ -431,7 +464,7 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { private var choosingStickerDisposable: Disposable? - override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, peerId: PeerId?) { self.containerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() @@ -472,7 +505,7 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { self.backgroundContainerNode = ASDisplayNode() - self.stickersNode = InlineReactionSearchStickersNode(context: context, theme: theme, strings: strings) + self.stickersNode = InlineReactionSearchStickersNode(context: context, theme: theme, strings: strings, peerId: peerId) super.init(context: context, theme: theme, strings: strings, fontSize: fontSize) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 003c38a2b8..a3c4762728 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -148,6 +148,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil)) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 738612a34f..106d7ec186 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2183,6 +2183,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 664c5d6fb8..f738c6c8a7 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -453,7 +453,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) forwardAccessoryPanelNode.updateState(size: size, inset: layout.safeInsets.left, interfaceState: self.presentationInterfaceState) forwardAccessoryPanelNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState) - let panelFrame = CGRect(x: layout.safeInsets.left, y: layout.size.height - (textPanelHeight ?? 0.0) - size.height, width: size.width - layout.safeInsets.left - layout.safeInsets.right, height: size.height) + let panelFrame = CGRect(x: 0.0, y: layout.size.height - (textPanelHeight ?? 0.0) - size.height, width: size.width, height: size.height) accessoryHeight = size.height if forwardAccessoryPanelNode.frame.width.isZero { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 96b0cfea60..70a535c60a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1295,6 +1295,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, getMessageTransitionNode: { return nil }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,