mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various Improvements
This commit is contained in:
parent
151a472294
commit
1f285271b2
@ -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";
|
||||
|
@ -34,6 +34,8 @@ swift_library(
|
||||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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<String?>()
|
||||
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
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -925,8 +925,21 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}))
|
||||
}
|
||||
|
||||
func transitionToProgressWithValue(signal: Signal<Float?, NoError>) {
|
||||
func transitionToProgressWithValue(signal: Signal<Float?, NoError>, dismissImmediately: Bool = false) {
|
||||
self.inputFieldNode.deactivateInput()
|
||||
|
||||
if dismissImmediately {
|
||||
self.animateOut(shared: true, completion: {})
|
||||
|
||||
self.shareDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
|
||||
}, completed: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
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)
|
||||
@ -967,4 +980,5 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<PeerId>?
|
||||
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?
|
||||
@ -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),
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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<TelegramAccountManagerTypes>) -> 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<TelegramAccountManagerTypes>, count: Int = 1, timestamp: Int32) -> Signal<Int, NoError> {
|
||||
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<TelegramAccountManagerTypes>) -> Signal<Void, NoError> {
|
||||
return accountManager.transaction { transaction -> Void in
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})))
|
||||
|
@ -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
|
||||
|
@ -530,6 +530,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, updateChoosingSticker: { _ in
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
@ -156,6 +156,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, updateChoosingSticker: { _ in
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -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
|
||||
f(.default)
|
||||
|
||||
let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds)
|
||||
})),
|
||||
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)
|
||||
})))
|
||||
|
||||
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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -2183,6 +2183,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, updateChoosingSticker: { _ in
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -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 {
|
||||
|
@ -1295,6 +1295,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, updateChoosingSticker: { _ in
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
Loading…
x
Reference in New Issue
Block a user