diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index be3d8a4d89..eb5d35028a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6718,8 +6718,11 @@ Sorry for the inconvenience."; "Conversation.Theme.Apply" = "Apply Theme"; "Conversation.Theme.NoTheme" = "No\nTheme"; "Conversation.Theme.Reset" = "Reset Theme for This Chat"; +"Conversation.Theme.DontSetTheme" = "Do Not Set Theme"; "Conversation.Theme.SwitchToDark" = "Switch to dark appearance"; "Conversation.Theme.SwitchToLight" = "Switch to light appearance"; +"Conversation.Theme.DismissAlert" = "Do you want to apply the selected theme to the chat?"; +"Conversation.Theme.DismissAlertApply" = "Apply"; "Notification.ChangedTheme" = "%1$@ changed chat theme to %2$@"; "Notification.DisabledTheme" = "%@ disabled chat theme"; @@ -6777,7 +6780,7 @@ Telegram offers free and unlimited service to hundreds of millions of users, whi [url] Ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together."; "SponsoredMessageInfo.Action" = "Learn More"; -"SponsoredMessageInfo.ActionUrl" = "https://telegram.org/ads"; +"SponsoredMessageInfo.Url" = "https://telegram.org/ads"; "Chat.NavigationNoChannels" = "You have no unread channels"; @@ -6816,3 +6819,10 @@ Ads should no longer be synonymous with abuse of user privacy. Let us redefine h "VideoChat.RecordingSaved" = "Video chat recording saved to **Saved Messages**."; "LiveStream.RecordingSaved" = "Live stream recording saved to **Saved Messages**."; + +"ChatContextMenu.MessageViewsPrivacyTip" = "To protect privacy, views are only stored for 7 days."; + +"MESSAGE_NOTHEME" = "%1$@ changed theme to default one"; +"CHAT_MESSAGE_NOTHEME" = "%1$@ set theme to default one in the group %2$@"; + +"Activity.EnjoyingAnimations" = "enjoying %@ animations"; diff --git a/build-system/GenerateStrings/GenerateStrings.py b/build-system/GenerateStrings/GenerateStrings.py index 52a54af3b1..be3033ae34 100644 --- a/build-system/GenerateStrings/GenerateStrings.py +++ b/build-system/GenerateStrings/GenerateStrings.py @@ -243,7 +243,7 @@ def generate(header_path: str, implementation_path: str, data_path: str, entries formatted_accessors += ''' static _FormattedString * _Nonnull getFormatted{num_arguments}(_PresentationStrings * _Nonnull strings, uint32_t keyId{arguments_string}) {{ - NSString *formatString = getSingle(strings, strings->_idToKey[@(keyId)]); + NSString *formatString = getSingle(strings, strings->_idToKey[@(keyId)], nil); NSArray<_FormattedStringRange *> *argumentRanges = extractArgumentRanges(formatString); return formatWithArgumentRanges(formatString, argumentRanges, @[{arguments_array}]); }} @@ -421,8 +421,8 @@ static _FormattedString * _Nonnull formatWithArgumentRanges( return [[_FormattedString alloc] initWithString:result ranges:resultingRanges]; } -static NSString * _Nonnull getPluralizationSuffix(_PresentationStrings * _Nonnull strings, int32_t value) { - NumberPluralizationForm pluralizationForm = numberPluralizationForm(strings.lc, value); +static NSString * _Nonnull getPluralizationSuffix(uint32_t lc, int32_t value) { + NumberPluralizationForm pluralizationForm = numberPluralizationForm(lc, value); switch (pluralizationForm) { case NumberPluralizationFormZero: { return @"_0"; @@ -445,10 +445,14 @@ static NSString * _Nonnull getPluralizationSuffix(_PresentationStrings * _Nonnul } } -static NSString * _Nonnull getSingle(_PresentationStrings * _Nonnull strings, NSString * _Nonnull key) { - NSString *result = strings.primaryComponent.dict[key]; - if (!result) { - result = strings.secondaryComponent.dict[key]; +static NSString * _Nonnull getSingle(_PresentationStrings * _Nullable strings, NSString * _Nonnull key, + bool * _Nullable isFound) { + NSString *result = nil; + if (strings) { + result = strings.primaryComponent.dict[key]; + if (!result) { + result = strings.secondaryComponent.dict[key]; + } } if (!result) { static NSDictionary *fallbackDict = nil; @@ -472,18 +476,31 @@ static NSString * _Nonnull getSingle(_PresentationStrings * _Nonnull strings, NS } if (!result) { result = key; + if (isFound) { + *isFound = false; + } + } else { + if (isFound) { + *isFound = true; + } } return result; } static NSString * _Nonnull getSingleIndirect(_PresentationStrings * _Nonnull strings, uint32_t keyId) { - return getSingle(strings, strings->_idToKey[@(keyId)]); + return getSingle(strings, strings->_idToKey[@(keyId)], nil); } static NSString * _Nonnull getPluralized(_PresentationStrings * _Nonnull strings, NSString * _Nonnull key, int32_t value) { - NSString *parsedKey = [[NSString alloc] initWithFormat:@"%@%@", key, getPluralizationSuffix(strings, value)]; - NSString *formatString = getSingle(strings, parsedKey); + NSString *parsedKey = [[NSString alloc] initWithFormat:@"%@%@", key, getPluralizationSuffix(strings.lc, value)]; + bool isFound = false; + NSString *formatString = getSingle(strings, parsedKey, &isFound); + if (!isFound) { + // fall back to English + parsedKey = [[NSString alloc] initWithFormat:@"%@%@", key, getPluralizationSuffix(0x656e, value)]; + formatString = getSingle(nil, parsedKey, nil); + } NSString *stringValue = formatNumberWithGroupingSeparator(strings.groupingSeparator, value); NSArray<_FormattedStringRange *> *argumentRanges = extractArgumentRanges(formatString); return formatWithArgumentRanges(formatString, argumentRanges, @[stringValue]).string; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 3543b3664c..335fe15d45 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -22,10 +22,11 @@ public final class ChatMessageItemAssociatedData: Equatable { public let contactsPeerIds: Set public let channelDiscussionGroup: ChannelDiscussionGroupStatus public let animatedEmojiStickers: [String: [StickerPackItem]] + public let additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] public let forcedResourceStatus: FileMediaResourceStatus? public let currentlyPlayingMessageId: EngineMessage.Index? - public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil) { + public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions @@ -33,6 +34,7 @@ public final class ChatMessageItemAssociatedData: Equatable { self.contactsPeerIds = contactsPeerIds self.channelDiscussionGroup = channelDiscussionGroup self.animatedEmojiStickers = animatedEmojiStickers + self.additionalAnimatedEmojiStickers = additionalAnimatedEmojiStickers self.forcedResourceStatus = forcedResourceStatus self.currentlyPlayingMessageId = currentlyPlayingMessageId } @@ -59,6 +61,9 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.animatedEmojiStickers != rhs.animatedEmojiStickers { return false } + if lhs.additionalAnimatedEmojiStickers != rhs.additionalAnimatedEmojiStickers { + return false + } if lhs.forcedResourceStatus != rhs.forcedResourceStatus { return false } diff --git a/submodules/AdUI/Sources/AdInfoScreen.swift b/submodules/AdUI/Sources/AdInfoScreen.swift index 61cf7923de..c951e7d705 100644 --- a/submodules/AdUI/Sources/AdInfoScreen.swift +++ b/submodules/AdUI/Sources/AdInfoScreen.swift @@ -106,14 +106,14 @@ public final class AdInfoScreen: ViewController { if !didAddUrl { didAddUrl = true - items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_ActionUrl, color: self.presentationData.theme.list.itemAccentColor, action: { + items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: { openUrl?() }))) } } if !didAddUrl { didAddUrl = true - items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_ActionUrl, color: self.presentationData.theme.list.itemAccentColor, action: { + items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: { openUrl?() }))) } @@ -138,7 +138,7 @@ public final class AdInfoScreen: ViewController { guard let strongSelf = self else { return } - strongSelf.context.sharedContext.applicationBindings.openUrl(strongSelf.presentationData.strings.SponsoredMessageInfo_ActionUrl) + strongSelf.context.sharedContext.applicationBindings.openUrl(strongSelf.presentationData.strings.SponsoredMessageInfo_Url) } } diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 6bef3c79bd..5806283b94 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -830,6 +830,8 @@ public final class AnimatedStickerNode: ASDisplayNode { } } + public var isPlayingChanged: (Bool) -> Void = { _ in } + override public init() { self.queue = sharedQueue self.eventsNode = AnimatedStickerNodeDisplayEvents() @@ -959,6 +961,8 @@ public final class AnimatedStickerNode: ASDisplayNode { } else{ self.pause() } + + self.isPlayingChanged(isPlaying) } let canDisplayFirstFrame = self.automaticallyLoadFirstFrame && self.isDisplaying if self.canDisplayFirstFrame != canDisplayFirstFrame { @@ -1046,7 +1050,9 @@ public final class AnimatedStickerNode: ASDisplayNode { if frame.isLastFrame { var stopped = false var stopNow = false - if case .once = strongSelf.playbackMode { + if case .still = strongSelf.playbackMode { + stopNow = true + } else if case .once = strongSelf.playbackMode { stopNow = true } else if case let .count(count) = strongSelf.playbackMode { strongSelf.currentLoopCount += 1 @@ -1143,7 +1149,9 @@ public final class AnimatedStickerNode: ASDisplayNode { if frame.isLastFrame { var stopped = false var stopNow = false - if case .once = strongSelf.playbackMode { + if case .still = strongSelf.playbackMode { + stopNow = true + } else if case .once = strongSelf.playbackMode { stopNow = true } else if case let .count(count) = strongSelf.playbackMode { strongSelf.currentLoopCount += 1 diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index a034b855d5..edc3aa69c7 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -24,8 +24,11 @@ public final class AnimationNode : ASDisplayNode { return self.animationView()?.isAnimationPlaying ?? false } + private var currentParams: (String?, [String: UIColor]?)? + public init(animation: String? = nil, colors: [String: UIColor]? = nil, scale: CGFloat = 1.0) { self.scale = scale + self.currentParams = (animation, colors) super.init() @@ -80,11 +83,23 @@ public final class AnimationNode : ASDisplayNode { }) } + public func makeCopy(colors: [String: UIColor]? = nil, progress: CGFloat? = nil) -> AnimationNode? { + guard let (animation, currentColors) = self.currentParams else { + return nil + } + let animationNode = AnimationNode(animation: animation, colors: colors ?? currentColors, scale: 1.0) + animationNode.animationView()?.play(fromProgress: progress ?? (self.animationView()?.animationProgress ?? 0.0), toProgress: 1.0, withCompletion: { [weak animationNode] _ in + animationNode?.completion?() + }) + return animationNode + } + public func seekToEnd() { self.animationView()?.animationProgress = 1.0 } public func setAnimation(name: String, colors: [String: UIColor]? = nil) { + self.currentParams = (name, colors) if let url = getAppBundle().url(forResource: name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { self.didPlay = false self.animationView()?.sceneModel = composition diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index df5ed91372..c7a01533bd 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -358,7 +358,7 @@ public final class CallListController: TelegramBaseController { } } - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(items), reactionItems: [], gesture: nil) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: nil) self.presentInGlobalOverlay(contextController) } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index a55d77066e..fcfb5ffe9c 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -250,10 +250,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined), minHeight: nil) + c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) - c.setItems(.single(updatedItems), minHeight: nil) + c.setItems(.single(ContextController.Items(items: updatedItems)), minHeight: nil) }))) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 3bdf073c35..980ad4debf 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -839,12 +839,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case let .groupReference(groupId, _, _, _, _): let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupId, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupId, chatListController: strongSelf), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: archiveContextMenuItems(context: strongSelf.context, groupId: groupId, chatListController: strongSelf) |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) case let .peer(_, peer, _, _, _, _, _, _, promoInfo, _, _, _): let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -868,7 +868,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -1095,7 +1095,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }))) } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode, keepInPlace: keepInPlace)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode, keepInPlace: keepInPlace)), items: .single(ContextController.Items(items: items)), reactionItems: [], recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } @@ -2842,7 +2842,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatListTabBarContextExtractedContentSource(controller: strongSelf, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), reactionItems: [], recognizer: nil, gesture: gesture) strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 10d45967ae..ff32dc6c29 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -737,7 +737,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return items } - let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items, reactionItems: [], recognizer: nil, gesture: gesture) + let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], recognizer: nil, gesture: gesture) self.presentInGlobalOverlay?(controller, nil) } @@ -797,7 +797,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay?(contextController, nil) case .instantPage: break diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 3cce18b827..1fb071c560 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1724,6 +1724,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.textNode.frame = textNodeFrame var animateInputActivitiesFrame = false + let inputActivities = inputActivities?.filter({ + switch $0.1 { + case .speakingInGroupCall, .interactingWithEmoji, .seeingEmojiInteraction: + return false + default: + return true + } + }) + if let inputActivities = inputActivities, !inputActivities.isEmpty { if strongSelf.inputActivitiesNode.supernode == nil { strongSelf.contextContainer.addSubnode(strongSelf.inputActivitiesNode) diff --git a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift index d4887c62ab..e2669b386b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift @@ -61,7 +61,7 @@ final class ChatListInputActivitiesNode: ASDisplayNode { text = strings.DialogList_Typing case .choosingSticker: text = strings.Activity_ChoosingSticker - case .speakingInGroupCall: + case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji: text = "" } let string = NSAttributedString(string: text, font: textFont, textColor: color) @@ -81,6 +81,8 @@ final class ChatListInputActivitiesNode: ASDisplayNode { state = .typingText(string, lightColor) case .choosingSticker: state = .choosingSticker(string, lightColor) + case .seeingEmojiInteraction, .interactingWithEmoji: + state = .none } } else { let text: String @@ -105,7 +107,7 @@ final class ChatListInputActivitiesNode: ASDisplayNode { text = strings.DialogList_SingleTypingSuffix(peerTitle).string case .choosingSticker: text = strings.DialogList_SingleChoosingStickerSuffix(peerTitle).string - case .speakingInGroupCall: + case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji: text = "" } } else { @@ -128,6 +130,8 @@ final class ChatListInputActivitiesNode: ASDisplayNode { state = .typingText(string, lightColor) case .choosingSticker: state = .choosingSticker(string, lightColor) + case .seeingEmojiInteraction, .interactingWithEmoji: + state = .none } } } else { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 85b52e9645..fa8f33eb82 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -175,7 +175,7 @@ final class ContactsControllerNode: ASDisplayNode { } let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: contactContextMenuItems(context: self.context, peerId: peer.id, contactsController: contactsController) |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) contactsController.presentInGlobalOverlay(contextController) } diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 1ec0352676..9a40d003cc 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -324,20 +324,26 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode { private let iconNode: ASImageNode private let text: String - private let targetSelectionIndex: Int + private let targetSelectionIndex: Int? - init(presentationData: PresentationData) { + init(presentationData: PresentationData, tip: ContextController.Tip) { self.presentationData = presentationData self.textNode = TextNode() - - var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip - if let range = rawText.range(of: "|") { - rawText.removeSubrange(range) - self.text = rawText - self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound - } else { - self.text = rawText - self.targetSelectionIndex = 1 + + switch tip { + case .textSelection: + var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip + if let range = rawText.range(of: "|") { + rawText.removeSubrange(range) + self.text = rawText + self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound + } else { + self.text = rawText + self.targetSelectionIndex = 1 + } + case .messageViewsPrivacy: + self.text = self.presentationData.strings.ChatContextMenu_MessageViewsPrivacyTip + self.targetSelectionIndex = nil } self.iconNode = ASImageNode() @@ -430,13 +436,13 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode { } func animateIn() { - if let textSelectionNode = self.textSelectionNode { + if let textSelectionNode = self.textSelectionNode, let targetSelectionIndex = self.targetSelectionIndex { textSelectionNode.pretendInitiateSelection() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in guard let strongSelf = self else { return } - strongSelf.textSelectionNode?.pretendExtendSelection(to: strongSelf.targetSelectionIndex) + strongSelf.textSelectionNode?.pretendExtendSelection(to: targetSelectionIndex) }) } } @@ -463,7 +469,7 @@ final class ContextActionsContainerNode: ASDisplayNode { return self.additionalActionsNode != nil } - init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) { + init(presentationData: PresentationData, items: ContextController.Items, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) { self.blurBackground = blurBackground self.shadowNode = ASImageNode() self.shadowNode.displaysAsynchronously = false @@ -473,7 +479,7 @@ final class ContextActionsContainerNode: ASDisplayNode { self.shadowNode.isHidden = true var items = items - if let firstItem = items.first, case let .custom(_, additional) = firstItem, additional { + if let firstItem = items.items.first, case let .custom(_, additional) = firstItem, additional { let additionalShadowNode = ASImageNode() additionalShadowNode.displaysAsynchronously = false additionalShadowNode.displayWithoutProcessing = true @@ -483,15 +489,15 @@ final class ContextActionsContainerNode: ASDisplayNode { self.additionalShadowNode = additionalShadowNode self.additionalActionsNode = InnerActionsContainerNode(presentationData: presentationData, items: [firstItem], getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground) - items.removeFirst() + items.items.removeFirst() } else { self.additionalShadowNode = nil self.additionalActionsNode = nil } - self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground) - if displayTextSelectionTip { - let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData) + self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: items.items, getController: getController, actionSelected: actionSelected, feedbackTap: feedbackTap, blurBackground: blurBackground) + if let tip = items.tip { + let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: presentationData, tip: tip) textSelectionTipNode.isUserInteractionEnabled = false self.textSelectionTipNode = textSelectionTipNode } else { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index bc8522a868..b6eebef7c0 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -14,9 +14,9 @@ public protocol ContextControllerProtocol { var useComplexItemsTransitionAnimation: Bool { get set } var immediateItemsTransitionAnimation: Bool { get set } - func getActionsMinHeight() -> CGFloat? - func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?) - func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?, previousActionsTransition: ContextController.PreviousActionsTransition) + func getActionsMinHeight() -> ContextController.ActionsHeight? + func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?) + func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) func dismiss(completion: (() -> Void)?) } @@ -117,7 +117,7 @@ private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIV private final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private var presentationData: PresentationData private let source: ContextContentSource - private var items: Signal<[ContextMenuItem], NoError> + private var items: Signal private let beginDismiss: (ContextMenuActionResult) -> Void private let reactionSelected: (ReactionContextItem.Reaction) -> Void private let beganAnimatingOut: () -> Void @@ -125,14 +125,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi fileprivate var dismissedForCancel: (() -> Void)? private let getController: () -> ContextControllerProtocol? private weak var gesture: ContextGesture? - private var displayTextSelectionTip: Bool private var didSetItemsReady = false let itemsReady = Promise() let contentReady = Promise() - private var currentItems: [ContextMenuItem]? - private var currentActionsMinHeight: CGFloat? + private var currentItems: ContextController.Items? + private var currentActionsMinHeight: ContextController.ActionsHeight? private var validLayout: ContainerViewLayout? @@ -169,7 +168,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let blurBackground: Bool - init(account: Account, controller: ContextController, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (ReactionContextItem.Reaction) -> Void, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void, displayTextSelectionTip: Bool) { + init(account: Account, controller: ContextController, presentationData: PresentationData, source: ContextContentSource, items: Signal, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (ReactionContextItem.Reaction) -> Void, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void) { self.presentationData = presentationData self.source = source self.items = items @@ -178,7 +177,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.beganAnimatingOut = beganAnimatingOut self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation self.gesture = gesture - self.displayTextSelectionTip = displayTextSelectionTip self.getController = { [weak controller] in return controller @@ -231,13 +229,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } self.blurBackground = blurBackground - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: [], getController: { [weak controller] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(items: []), getController: { [weak controller] in return controller }, actionSelected: { result in beginDismiss(result) }, feedbackTap: { feedbackTap?() - }, displayTextSelectionTip: self.displayTextSelectionTip, blurBackground: blurBackground) + }, blurBackground: blurBackground) if !reactionItems.isEmpty { let reactionContextNode = ReactionContextNode(account: account, theme: presentationData.theme, items: reactionItems) @@ -1171,15 +1169,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }) } - func getActionsMinHeight() -> CGFloat? { + func getActionsMinHeight() -> ContextController.ActionsHeight? { if !self.actionsContainerNode.bounds.height.isZero { - return self.actionsContainerNode.bounds.height + return ContextController.ActionsHeight( + minY: self.actionsContainerNode.frame.minY, + contentOffset: self.scrollNode.view.contentOffset.y + ) } else { return nil } } - func setItemsSignal(items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?, previousActionsTransition: ContextController.PreviousActionsTransition) { + func setItemsSignal(items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { self.items = items self.itemsDisposable.set((items |> deliverOnMainQueue).start(next: { [weak self] items in @@ -1190,7 +1191,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi })) } - private func setItems(items: [ContextMenuItem], minHeight: CGFloat?, previousActionsTransition: ContextController.PreviousActionsTransition) { + private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true { return } @@ -1199,24 +1200,24 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.currentActionsMinHeight = minHeight let previousActionsContainerNode = self.actionsContainerNode + let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view) self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in return self?.getController() }, actionSelected: { [weak self] result in self?.beginDismiss(result) }, feedbackTap: { [weak self] in self?.hapticFeedback.tap() - }, displayTextSelectionTip: self.displayTextSelectionTip, blurBackground: self.blurBackground) + }, blurBackground: self.blurBackground) self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) if let layout = self.validLayout { - self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode, previousActionsTransition: previousActionsTransition) + self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .spring), previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition) } else { previousActionsContainerNode.removeFromSupernode() } if !self.didSetItemsReady { self.didSetItemsReady = true - self.displayTextSelectionTip = false self.itemsReady.set(.single(true)) } } @@ -1228,11 +1229,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.actionsContainerNode.updateTheme(presentationData: presentationData) if let validLayout = self.validLayout { - self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) + self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil, previousActionsContainerFrame: nil) } } - func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) { + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsContainerFrame: CGRect? = nil, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) { if self.isAnimatingOut { return } @@ -1295,7 +1296,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) - let adjustedActionsSize = CGSize(width: realActionsSize.width, height: max(realActionsSize.height, self.currentActionsMinHeight ?? 0.0)) + let adjustedActionsSize = realActionsSize self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) let contentSize = originalProjectedContentViewFrame.1.size @@ -1375,7 +1376,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition) - let adjustedActionsSize = CGSize(width: realActionsSize.width, height: max(realActionsSize.height, self.currentActionsMinHeight ?? 0.0)) + let adjustedActionsSize = realActionsSize self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) let contentSize = originalProjectedContentViewFrame.1.size @@ -1383,7 +1384,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) let preferredActionsX: CGFloat - let originalActionsY: CGFloat + var originalActionsY: CGFloat if centerVertically { originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) preferredActionsX = originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width @@ -1395,6 +1396,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi preferredActionsX = originalProjectedContentViewFrame.1.minX } + if let currentActionsMinHeight = self.currentActionsMinHeight { + originalActionsY = currentActionsMinHeight.minY + } + var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX let originalContentY: CGFloat @@ -1417,7 +1422,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if keepInPlace { contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalActionsFrame.minY + contentTopInset) } else { - contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) + if self.currentActionsMinHeight != nil { + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom)) + } else { + contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom) - originalContentFrame.minY + contentTopInset) + } } var overflowOffset: CGFloat @@ -1475,12 +1484,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) if isInitialLayout { + //let previousContentOffset = self.scrollNode.view.contentOffset.y if !keepInPlace { - self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + if let currentActionsMinHeight = self.currentActionsMinHeight { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: currentActionsMinHeight.contentOffset) + } else { + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) + } } let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) + var offset: CGFloat = 0.0 + //offset -= previousContentOffset - self.scrollNode.view.contentOffset.y + offset += previousContainerFrame.minY - currentContainerFrame.minY + transition.animatePositionAdditive(node: self.contentContainerNode, offset: CGPoint(x: 0.0, y: offset)) if overflowOffset < 0.0 { - transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) + let _ = currentContainerFrame + let _ = previousContainerFrame } } @@ -1650,6 +1669,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } else { + if let previousActionsContainerFrame = previousActionsContainerFrame { + previousActionsContainerNode.frame = self.view.convert(previousActionsContainerFrame, to: self.actionsContainerNode.view.superview!) + } + switch previousActionsTransition { case .scale: transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1) @@ -1660,33 +1683,26 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) case let .slide(forward): - if case .compact = layout.metrics.widthClass { - if forward { - transition.updatePosition(node: previousActionsContainerNode, position: CGPoint(x: -previousActionsContainerNode.bounds.width / 2.0, y: previousActionsContainerNode.position.y), completion: { [weak previousActionsContainerNode] _ in - previousActionsContainerNode?.removeFromSupernode() - }) - transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: layout.size.width + self.actionsContainerNode.bounds.width / 2.0 - self.actionsContainerNode.position.x, y: 0.0)) - } else { - transition.updatePosition(node: previousActionsContainerNode, position: CGPoint(x: layout.size.width + previousActionsContainerNode.bounds.width / 2.0, y: previousActionsContainerNode.position.y), completion: { [weak previousActionsContainerNode] _ in - previousActionsContainerNode?.removeFromSupernode() - }) - transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: -self.actionsContainerNode.bounds.width / 2.0 - self.actionsContainerNode.position.x, y: 0.0)) - } + let deltaY = self.actionsContainerNode.frame.minY - previousActionsContainerNode.frame.minY + var previousNodePosition = previousActionsContainerNode.position.offsetBy(dx: 0.0, dy: deltaY) + let additionalHorizontalOffset: CGFloat = 20.0 + let currentNodeOffset: CGFloat + if forward { + previousNodePosition = previousNodePosition.offsetBy(dx: -previousActionsContainerNode.frame.width / 2.0 - additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) + currentNodeOffset = self.actionsContainerNode.bounds.width / 2.0 + additionalHorizontalOffset } else { - let offset: CGFloat - if forward { - offset = previousActionsContainerNode.bounds.width - } else { - offset = -previousActionsContainerNode.bounds.width - } - transition.updatePosition(node: previousActionsContainerNode, position: previousActionsContainerNode.position.offsetBy(dx: -offset, dy: 0.0)) - previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in - previousActionsContainerNode?.removeFromSupernode() - }) - - transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: offset, y: 0.0)) - self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + previousNodePosition = previousNodePosition.offsetBy(dx: previousActionsContainerNode.frame.width / 2.0 + additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) + currentNodeOffset = -self.actionsContainerNode.bounds.width / 2.0 - additionalHorizontalOffset } + transition.updatePosition(node: previousActionsContainerNode, position: previousNodePosition) + transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.01) + previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in + previousActionsContainerNode?.removeFromSupernode() + }) + + transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: currentNodeOffset, y: -deltaY - self.actionsContainerNode.bounds.height / 2.0)) + transition.animateTransformScale(node: self.actionsContainerNode, from: 0.01) + self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } else { @@ -1863,15 +1879,45 @@ public enum ContextContentSource { } public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol { + public struct Items { + public var items: [ContextMenuItem] + public var tip: Tip? + + public init(items: [ContextMenuItem], tip: Tip? = nil) { + self.items = items + self.tip = tip + } + + public init() { + self.items = [] + self.tip = nil + } + } + public enum PreviousActionsTransition { case scale case slide(forward: Bool) } + public enum Tip { + case textSelection + case messageViewsPrivacy + } + + public final class ActionsHeight { + fileprivate let minY: CGFloat + fileprivate let contentOffset: CGFloat + + fileprivate init(minY: CGFloat, contentOffset: CGFloat) { + self.minY = minY + self.contentOffset = contentOffset + } + } + private let account: Account private var presentationData: PresentationData private let source: ContextContentSource - private var items: Signal<[ContextMenuItem], NoError> + private var items: Signal private var reactionItems: [ReactionContextItem] private let _ready = Promise() @@ -1881,7 +1927,6 @@ public final class ContextController: ViewController, StandalonePresentableContr private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? private weak var gesture: ContextGesture? - private let displayTextSelectionTip: Bool private var animatedDidAppear = false private var wasDismissed = false @@ -1903,7 +1948,7 @@ public final class ContextController: ViewController, StandalonePresentableContr private var shouldBeDismissedDisposable: Disposable? - public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, displayTextSelectionTip: Bool = false) { + public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal, reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) { self.account = account self.presentationData = presentationData self.source = source @@ -1911,7 +1956,6 @@ public final class ContextController: ViewController, StandalonePresentableContr self.reactionItems = reactionItems self.recognizer = recognizer self.gesture = gesture - self.displayTextSelectionTip = displayTextSelectionTip super.init(navigationBarPresentationData: nil) @@ -1982,7 +2026,7 @@ public final class ContextController: ViewController, StandalonePresentableContr default: break } - }, displayTextSelectionTip: self.displayTextSelectionTip) + }) self.controllerNode.dismissedForCancel = self.dismissedForCancel self.displayNodeDidLoad() @@ -2010,21 +2054,21 @@ public final class ContextController: ViewController, StandalonePresentableContr } } - public func getActionsMinHeight() -> CGFloat? { + public func getActionsMinHeight() -> ContextController.ActionsHeight? { if self.isNodeLoaded { return self.controllerNode.getActionsMinHeight() } return nil } - public func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?) { + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?) { self.items = items if self.isNodeLoaded { self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale) } } - public func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?, previousActionsTransition: ContextController.PreviousActionsTransition) { + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { self.items = items if self.isNodeLoaded { self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) diff --git a/submodules/ContextUI/Sources/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift index 88360248f1..9976d5bff9 100644 --- a/submodules/ContextUI/Sources/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -34,14 +34,14 @@ public final class PeekController: ViewController, ContextControllerProtocol { public var useComplexItemsTransitionAnimation: Bool = false public var immediateItemsTransitionAnimation = false - public func getActionsMinHeight() -> CGFloat? { + public func getActionsMinHeight() -> ContextController.ActionsHeight? { return nil } - public func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?) { + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?) { } - public func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?, previousActionsTransition: ContextController.PreviousActionsTransition) { + public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { } private var controllerNode: PeekControllerNode { diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index 6ad81cb574..13156362aa 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -72,13 +72,13 @@ final class PeekControllerNode: ViewControllerTracingNode { var feedbackTapImpl: (() -> Void)? var activatedActionImpl: (() -> Void)? - self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: content.menuItems(), getController: { [weak controller] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(items: content.menuItems()), getController: { [weak controller] in return controller }, actionSelected: { result in activatedActionImpl?() }, feedbackTap: { feedbackTapImpl?() - }, displayTextSelectionTip: false, blurBackground: true) + }, blurBackground: true) self.actionsContainerNode.alpha = 0.0 super.init() @@ -328,13 +328,13 @@ final class PeekControllerNode: ViewControllerTracingNode { self.contentNodeHasValidLayout = false let previousActionsContainerNode = self.actionsContainerNode - self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: content.menuItems(), getController: { [weak self] in + self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: ContextController.Items(items: content.menuItems()), getController: { [weak self] in return self?.controller }, actionSelected: { [weak self] result in self?.requestDismiss() }, feedbackTap: { [weak self] in self?.hapticFeedback.tap() - }, displayTextSelectionTip: false, blurBackground: true) + }, blurBackground: true) self.actionsContainerNode.alpha = 0.0 self.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) previousActionsContainerNode.removeFromSupernode() diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index c462c472ba..9a4ab417cb 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -310,6 +310,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private final var actionsForVSync: [() -> ()] = [] private final var inVSync = false + private var tapGestureRecognizer: UITapGestureRecognizer? + public final var tapped: (() -> Void)? { + didSet { + self.tapGestureRecognizer?.isEnabled = self.tapped != nil + } + } + private let frictionSlider = UISlider() private let springSlider = UISlider() private let freeResistanceSlider = UISlider() @@ -386,7 +393,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.view.addSubview(self.scroller) self.scroller.panGestureRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) - + let trackingRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.trackingGesture(_:))) trackingRecognizer.delegate = self trackingRecognizer.cancelsTouchesInView = false @@ -420,6 +427,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self?.updateReordering(offset: offset) })) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + tapGestureRecognizer.isEnabled = false + tapGestureRecognizer.delegate = self + self.view.addGestureRecognizer(tapGestureRecognizer) + self.tapGestureRecognizer = tapGestureRecognizer + self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common) @@ -449,6 +462,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.reorderFeedbackDisposable?.dispose() } + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + self.tapped?() + } + private func displayLinkEvent() { self.updateAnimations() } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 95e12d23b3..754c243979 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2032,7 +2032,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) self.isShowingContextMenuPromise.set(true) controller.presentInGlobalOverlay(contextController) @@ -2087,7 +2087,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - c.setItems(strongSelf.contextMenuSpeedItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuSpeedItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) if let (message, _, _) = strongSelf.contentInfo() { @@ -2206,7 +2206,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { c.dismiss(completion: nil) return } - c.setItems(strongSelf.contextMenuMainItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) return items diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index bd351f125b..f2d5a47df1 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -418,7 +418,7 @@ public final class InviteLinkInviteController: ViewController { }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }, copyLink: { [weak self] invite in UIPasteboard.general.string = invite.link diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index 507b469514..06303b08db 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -550,7 +550,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, createLink: { let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in @@ -714,7 +714,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: true)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: true)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, openAdmin: { admin in let controller = inviteLinkListController(context: context, peerId: peerId, admin: admin) diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 6a30a33fc5..818b16a77a 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -565,7 +565,7 @@ public final class InviteLinkViewController: ViewController { }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }) diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index cb386cc0c5..27b8f92b3a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1050,7 +1050,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, manageInviteLinks: { let controller = inviteLinkListController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, admin: nil) diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/PeerInfoUI/Sources/PeerReportController.swift index 11606cc045..eb537851ff 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/PeerInfoUI/Sources/PeerReportController.swift @@ -158,7 +158,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro backAction(c) }))) } - contextController.setItems(.single(items), minHeight: nil) + contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) } else { contextController?.dismiss(completion: nil) parent.view.endEditing(true) diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 86e33afb2d..8f3aff66bb 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -494,7 +494,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { chatController.canReadHistory.set(false) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: peerNearbyContextMenuItems(context: context, peerId: peer.id, present: { c in presentControllerImpl?(c, nil) - }), reactionItems: [], gesture: gesture) + }) |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController) }, expandUsers: { expandedPromise.set(true) diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 8bb2ab8b58..a7d139facc 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -88,11 +88,15 @@ public final class Transaction { return self.postbox!.messageHistoryThreadHoleIndexTable.closest(peerId: peerId, threadId: threadId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1)) } - public func getThreadMessageCount(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, fromId: Int32?, toIndex: MessageIndex) -> Int? { + public func getThreadMessageCount(peerId: PeerId, threadId: Int64, namespace: MessageId.Namespace, fromIdExclusive: Int32?, toIndex: MessageIndex) -> Int? { assert(!self.disposed) let fromIndex: MessageIndex? - if let fromId = fromId { - fromIndex = self.postbox!.messageHistoryIndexTable.closestIndex(id: MessageId(peerId: peerId, namespace: namespace, id: fromId)) + if let fromIdExclusive = fromIdExclusive { + if let message = self.postbox?.getMessage(MessageId(peerId: peerId, namespace: namespace, id: fromIdExclusive)) { + fromIndex = message.index.peerLocalSuccessor() + } else { + fromIndex = self.postbox!.messageHistoryIndexTable.closestIndex(id: MessageId(peerId: peerId, namespace: namespace, id: fromIdExclusive))?.peerLocalSuccessor() + } } else { fromIndex = nil } diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index d8856e042d..8d3d9233cb 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -166,6 +166,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index e5751993cf..90f64cb3d1 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -804,7 +804,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }))) } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }, colorContextAction: { isCurrent, reference, accentColor, node, gesture in @@ -1041,7 +1041,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The } } } - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: themeController, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) presentInGlobalOverlayImpl?(contextController, nil) }) }) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 3367448abd..aa1b95f4f6 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -523,7 +523,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD }) }))) - let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(ChannelStatsContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) controller.presentInGlobalOverlay(contextController) } return controller diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 1dcd653361..9a4d051732 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -182,6 +182,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-606432698] = { return Api.SendMessageAction.parse_sendMessageHistoryImportAction($0) } dict[-1336228175] = { return Api.SendMessageAction.parse_sendMessageChooseStickerAction($0) } + dict[1781674934] = { return Api.SendMessageAction.parse_sendMessageEmojiInteraction($0) } + dict[-1234857938] = { return Api.SendMessageAction.parse_sendMessageEmojiInteractionSeen($0) } dict[-1137792208] = { return Api.PrivacyKey.parse_privacyKeyStatusTimestamp($0) } dict[1343122938] = { return Api.PrivacyKey.parse_privacyKeyChatInvite($0) } dict[1030105979] = { return Api.PrivacyKey.parse_privacyKeyPhoneCall($0) } @@ -667,6 +669,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2044933984] = { return Api.InputStickerSet.parse_inputStickerSetShortName($0) } dict[42402760] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmoji($0) } dict[-427863538] = { return Api.InputStickerSet.parse_inputStickerSetDice($0) } + dict[215889721] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmojiAnimations($0) } dict[-1231326505] = { return Api.messages.ChatAdminsWithInvites.parse_chatAdminsWithInvites($0) } dict[460632885] = { return Api.BotInfo.parse_botInfo($0) } dict[-2046910401] = { return Api.stickers.SuggestedShortName.parse_suggestedShortName($0) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 1a8c28588a..02e181a41b 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -4278,6 +4278,8 @@ public extension Api { case speakingInGroupCallAction case sendMessageHistoryImportAction(progress: Int32) case sendMessageChooseStickerAction + case sendMessageEmojiInteraction(emoticon: String, interaction: Api.DataJSON) + case sendMessageEmojiInteractionSeen(emoticon: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -4376,6 +4378,19 @@ public extension Api { buffer.appendInt32(-1336228175) } + break + case .sendMessageEmojiInteraction(let emoticon, let interaction): + if boxed { + buffer.appendInt32(1781674934) + } + serializeString(emoticon, buffer: buffer, boxed: false) + interaction.serialize(buffer, true) + break + case .sendMessageEmojiInteractionSeen(let emoticon): + if boxed { + buffer.appendInt32(-1234857938) + } + serializeString(emoticon, buffer: buffer, boxed: false) break } } @@ -4414,6 +4429,10 @@ public extension Api { return ("sendMessageHistoryImportAction", [("progress", progress)]) case .sendMessageChooseStickerAction: return ("sendMessageChooseStickerAction", []) + case .sendMessageEmojiInteraction(let emoticon, let interaction): + return ("sendMessageEmojiInteraction", [("emoticon", emoticon), ("interaction", interaction)]) + case .sendMessageEmojiInteractionSeen(let emoticon): + return ("sendMessageEmojiInteractionSeen", [("emoticon", emoticon)]) } } @@ -4513,6 +4532,33 @@ public extension Api { public static func parse_sendMessageChooseStickerAction(_ reader: BufferReader) -> SendMessageAction? { return Api.SendMessageAction.sendMessageChooseStickerAction } + public static func parse_sendMessageEmojiInteraction(_ reader: BufferReader) -> SendMessageAction? { + var _1: String? + _1 = parseString(reader) + var _2: Api.DataJSON? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.SendMessageAction.sendMessageEmojiInteraction(emoticon: _1!, interaction: _2!) + } + else { + return nil + } + } + public static func parse_sendMessageEmojiInteractionSeen(_ reader: BufferReader) -> SendMessageAction? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.SendMessageAction.sendMessageEmojiInteractionSeen(emoticon: _1!) + } + else { + return nil + } + } } public enum PrivacyKey: TypeConstructorDescription { @@ -16893,6 +16939,7 @@ public extension Api { case inputStickerSetShortName(shortName: String) case inputStickerSetAnimatedEmoji case inputStickerSetDice(emoticon: String) + case inputStickerSetAnimatedEmojiAnimations public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -16926,6 +16973,12 @@ public extension Api { buffer.appendInt32(-427863538) } serializeString(emoticon, buffer: buffer, boxed: false) + break + case .inputStickerSetAnimatedEmojiAnimations: + if boxed { + buffer.appendInt32(215889721) + } + break } } @@ -16942,6 +16995,8 @@ public extension Api { return ("inputStickerSetAnimatedEmoji", []) case .inputStickerSetDice(let emoticon): return ("inputStickerSetDice", [("emoticon", emoticon)]) + case .inputStickerSetAnimatedEmojiAnimations: + return ("inputStickerSetAnimatedEmojiAnimations", []) } } @@ -16987,6 +17042,9 @@ public extension Api { return nil } } + public static func parse_inputStickerSetAnimatedEmojiAnimations(_ reader: BufferReader) -> InputStickerSet? { + return Api.InputStickerSet.inputStickerSetAnimatedEmojiAnimations + } } public enum BotInfo: TypeConstructorDescription { diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 57aff7a548..9900650e24 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -555,7 +555,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi return } let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems() - let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) self.presentInGlobalOverlay?(contextController) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 083edd023c..a1a8c48bc7 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1787,7 +1787,7 @@ public final class VoiceChatController: ViewController { dismissPromise.set(true) } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(source), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) contextController.useComplexItemsTransitionAnimation = true strongSelf.controller?.presentInGlobalOverlay(contextController) }, getPeerVideo: { [weak self] endpointId, position in @@ -2444,7 +2444,7 @@ public final class VoiceChatController: ViewController { private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() if let controller = self.controller { - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) controller.presentInGlobalOverlay(contextController) } } @@ -2473,7 +2473,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuDisplayAsItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuDisplayAsItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) items.append(.separator) break @@ -2506,7 +2506,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuAudioItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuAudioItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) } @@ -2543,7 +2543,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuPermissionItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuPermissionItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) } } @@ -2803,7 +2803,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) return .single(items) } @@ -2898,7 +2898,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) return items } @@ -2944,7 +2944,7 @@ public final class VoiceChatController: ViewController { guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuMainItems(), minHeight: nil) + c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(items: $0) }, minHeight: nil) }))) } return .single(items) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index dcb0ea6f78..87b0cd6fcd 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1140,9 +1140,13 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeAppLogEventsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedNotificationSettingsBehaviors(postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedThemesUpdates(accountManager: accountManager, postbox: self.postbox, network: self.network).start()) + if !self.testingEnvironment { + self.managedOperationsDisposable.add(managedChatThemesUpdates(accountManager: accountManager, network: self.network).start()) + } if !self.supplementary { self.managedOperationsDisposable.add(managedAnimatedEmojiUpdates(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedAnimatedEmojiAnimationsUpdates(postbox: self.postbox, network: self.network).start()) } self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start()) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 759d4fb034..493d4e8b04 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -60,6 +60,8 @@ extension StickerPackReference { self = .animatedEmoji case let .inputStickerSetDice(emoticon): self = .dice(emoticon) + case .inputStickerSetAnimatedEmojiAnimations: + self = .animatedEmojiAnimations } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift index 1577d99a51..10ad50d969 100644 --- a/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedAnimatedEmojiUpdates.swift @@ -11,3 +11,11 @@ func managedAnimatedEmojiUpdates(postbox: Postbox, network: Network) -> Signal then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } + +func managedAnimatedEmojiAnimationsUpdates(postbox: Postbox, network: Network) -> Signal { + let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .animatedEmojiAnimations, forceActualized: true) + |> mapToSignal { _ -> Signal in + return .complete() + } + return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift index f06531a28b..b8fac6d1bf 100644 --- a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift +++ b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift @@ -131,6 +131,10 @@ private func actionFromActivity(_ activity: PeerInputActivity?) -> Api.SendMessa return .speakingInGroupCallAction case .choosingSticker: return .sendMessageChooseStickerAction + case let .interactingWithEmoji(emoticon, interaction): + return .sendMessageEmojiInteraction(emoticon: emoticon, interaction: interaction?.apiDataJson ?? .dataJSON(data: "")) + case let .seeingEmojiInteraction(emoticon): + return .sendMessageEmojiInteractionSeen(emoticon: emoticon) } } else { return .sendMessageCancelAction diff --git a/submodules/TelegramCore/Sources/State/PeerInputActivity.swift b/submodules/TelegramCore/Sources/State/PeerInputActivity.swift index eff6f99446..d01342f8d7 100644 --- a/submodules/TelegramCore/Sources/State/PeerInputActivity.swift +++ b/submodules/TelegramCore/Sources/State/PeerInputActivity.swift @@ -1,6 +1,60 @@ import Foundation import TelegramApi +public struct EmojiInteraction: Equatable { + public struct Animation: Equatable { + public let index: Int + public let timeOffset: Float + + public init(index: Int, timeOffset: Float) { + self.index = index + self.timeOffset = timeOffset + } + } + + public let animations: [Animation] + + public init(animations: [Animation]) { + self.animations = animations + } + + public init?(apiDataJson: Api.DataJSON) { + if case let .dataJSON(string) = apiDataJson, let data = string.data(using: .utf8) { + do { + let decodedData = try JSONSerialization.jsonObject(with: data, options: []) + guard let item = decodedData as? [String: Any] else { + return nil + } + guard let animationsArray = item["a"] as? [Any] else { + return nil + } + var animations: [EmojiInteraction.Animation] = [] + for animationDict in animationsArray { + if let animationDict = animationDict as? [String: Any] { + if let index = animationDict["i"] as? Int, let timeOffset = animationDict["t"] as? Float { + animations.append(EmojiInteraction.Animation(index: index, timeOffset: timeOffset)) + } + } + } + self.animations = animations + } catch { + return nil + } + } else { + return nil + } + } + + public var apiDataJson: Api.DataJSON { + let dict = ["v": 1, "a": self.animations.map({ ["i": $0.index, "t": $0.timeOffset] })] as [String : Any] + if let data = try? JSONSerialization.data(withJSONObject: dict, options: []), let dataString = String(data: data, encoding: .utf8) { + return .dataJSON(data: dataString) + } else { + return .dataJSON(data: "") + } + } +} + public enum PeerInputActivity: Comparable { case typingText case uploadingFile(progress: Int32) @@ -12,6 +66,8 @@ public enum PeerInputActivity: Comparable { case uploadingInstantVideo(progress: Int32) case speakingInGroupCall(timestamp: Int32) case choosingSticker + case interactingWithEmoji(emoticon: String, interaction: EmojiInteraction?) + case seeingEmojiInteraction(emoticon: String) public var key: Int32 { switch self { @@ -35,6 +91,10 @@ public enum PeerInputActivity: Comparable { return 8 case .choosingSticker: return 9 + case .interactingWithEmoji: + return 10 + case .seeingEmojiInteraction: + return 11 } } @@ -70,6 +130,10 @@ extension PeerInputActivity { self = .choosingSticker case .sendMessageHistoryImportAction: return nil + case let .sendMessageEmojiInteraction(emoticon, interaction): + self = .interactingWithEmoji(emoticon: emoticon, interaction: EmojiInteraction(apiDataJson: interaction)) + case let .sendMessageEmojiInteractionSeen(emoticon): + self = .seeingEmojiInteraction(emoticon: emoticon) } } } diff --git a/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift b/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift index f7fa72537c..892b6f21c6 100644 --- a/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift +++ b/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift @@ -328,7 +328,9 @@ final class PeerInputActivityManager { let timeout: Double switch activity { - case .speakingInGroupCall: + case .interactingWithEmoji: + timeout = 2.0 + case .speakingInGroupCall, .seeingEmojiInteraction: timeout = 3.0 default: timeout = 8.0 diff --git a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift index 34eeb16369..1bf6f161ef 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeSavedStickersOperation.swift @@ -61,7 +61,7 @@ public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMe if !found { fetchReference = packReference } - case .animatedEmoji, .dice: + case .animatedEmoji, .animatedEmojiAnimations, .dice: break } if let fetchReference = fetchReference { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 3a57dba083..b21e42e3a7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -44,6 +44,7 @@ public struct Namespaces { public static let EmojiKeywords: Int32 = 2 public static let CloudAnimatedEmoji: Int32 = 3 public static let CloudDice: Int32 = 4 + public static let CloudAnimatedEmojiAnimations: Int32 = 5 } public struct OrderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 9b27539e27..9a667cf238 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -16,6 +16,7 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable { case name(String) case animatedEmoji case dice(String) + case animatedEmojiAnimations public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { @@ -27,6 +28,8 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable { self = .animatedEmoji case 3: self = .dice(decoder.decodeStringForKey("e", orElse: "🎲")) + case 4: + self = .animatedEmojiAnimations default: self = .name("") assertionFailure() @@ -47,6 +50,8 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable { case let .dice(emoji): encoder.encodeInt32(3, forKey: "r") encoder.encodeString(emoji, forKey: "e") + case .animatedEmojiAnimations: + encoder.encodeInt32(4, forKey: "r") } } @@ -76,6 +81,12 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable { } else { return false } + case .animatedEmojiAnimations: + if case .animatedEmojiAnimations = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift index a46d37b78b..963c0839df 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MessageReadStats.swift @@ -35,11 +35,16 @@ func _internal_messageReadStats(account: Account, id: MessageId) -> Signal mapToSignal { result, loadRemote, previousHash in @@ -187,7 +204,19 @@ func cachedStickerPack(transaction: Transaction, reference: StickerPackReference } if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info { return (info, cached.items, false) - } + } + case .animatedEmojiAnimations: + let namespace = Namespaces.ItemCollection.CloudAnimatedEmojiAnimations + let id: ItemCollectionId.Id = 0 + if let currentInfo = transaction.getItemCollectionInfo(collectionId: ItemCollectionId(namespace: namespace, id: id)) as? StickerPackCollectionInfo { + let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: namespace, id: id)) + if !items.isEmpty { + return (currentInfo, items, true) + } + } + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(ItemCollectionId(namespace: namespace, id: id)))) as? CachedStickerPack, let info = cached.info { + return (info, cached.items, false) + } } return nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index 81d1a3b615..017981b0d0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -19,6 +19,8 @@ extension StickerPackReference { return .inputStickerSetAnimatedEmoji case let .dice(emoji): return .inputStickerSetDice(emoticon: emoji) + case .animatedEmojiAnimations: + return .inputStickerSetAnimatedEmojiAnimations } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index 2ef5a55c64..d63fcc5538 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -40,6 +40,9 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference: case let .dice(emoji): collectionId = nil input = .inputStickerSetDice(emoticon: emoji) + case .animatedEmojiAnimations: + collectionId = nil + input = .inputStickerSetAnimatedEmojiAnimations } let localSignal: (ItemCollectionId) -> Signal<(ItemCollectionInfo, [ItemCollectionItem])?, NoError> = { collectionId in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index 97f7bb021d..84d65e7462 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -104,7 +104,7 @@ func _internal_getChatThemes(accountManager: AccountManager mapToSignal { current, hash -> Signal<[ChatTheme], NoError> in - if onlyCached { + if onlyCached && !current.isEmpty { return .single(current) } else { return .single(current) @@ -154,3 +154,11 @@ extension ChatTheme { } } } + +func managedChatThemesUpdates(accountManager: AccountManager, network: Network) -> Signal { + let poll = _internal_getChatThemes(accountManager: accountManager, network: network) + |> mapToSignal { _ -> Signal in + return .complete() + } + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/Utils/JSON.swift b/submodules/TelegramCore/Sources/Utils/JSON.swift index 49dbf2801d..eea17f436c 100644 --- a/submodules/TelegramCore/Sources/Utils/JSON.swift +++ b/submodules/TelegramCore/Sources/Utils/JSON.swift @@ -2,7 +2,6 @@ import Foundation import Postbox import TelegramApi - extension JSON { private init?(_ object: Any) { if let object = object as? JSONValue { diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 7556a8aadd..476abc6c55 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -169,6 +169,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case dismissedTrendingStickerPacks = 22 case chatSpecificThemesDarkPreviewTip = 23 case chatForwardOptionsTip = 24 + case messageViewsPrivacyTips = 25 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -292,6 +293,10 @@ private struct ApplicationSpecificNoticeKeys { static func chatTextSelectionTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.chatTextSelectionTip.key) } + + static func messageViewsPrivacyTips() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.messageViewsPrivacyTips.key) + } static func themeChangeTip() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.themeChangeTip.key) @@ -745,6 +750,28 @@ public struct ApplicationSpecificNotice { transaction.setNotice(ApplicationSpecificNoticeKeys.chatTextSelectionTip(), ApplicationSpecificCounterNotice(value: currentValue)) } } + + public static func getMessageViewsPrivacyTips(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.messageViewsPrivacyTips()) as? ApplicationSpecificCounterNotice { + return value.value + } else { + return 0 + } + } + } + + public static func incrementMessageViewsPrivacyTips(accountManager: AccountManager, count: Int32 = 1) -> Signal { + return accountManager.transaction { transaction -> Void in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.messageViewsPrivacyTips()) as? ApplicationSpecificCounterNotice { + currentValue = value.value + } + currentValue += count + + transaction.setNotice(ApplicationSpecificNoticeKeys.messageViewsPrivacyTips(), ApplicationSpecificCounterNotice(value: currentValue)) + } + } public static func getThemeChangeTip(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Bool in diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 86bffdc117..89c384e58c 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -168,7 +168,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti outgoingInactiveControlColor = outgoingAccent outgoingFileTitleColor = outgoingAccent outgoingPollsProgressColor = outgoingControlColor - outgoingSelectionColor = outgoingAccent.withMultiplied(hue: 1.0, saturation: 1.292, brightness: 0.871) + outgoingSelectionColor = outgoingAccent.withAlphaComponent(0.2) outgoingSelectionBaseColor = outgoingControlColor outgoingCheckColor = outgoingAccent } else { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7d7716bb0d..c16cca12ab 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -926,7 +926,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) ).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips in - guard let strongSelf = self, !actions.isEmpty else { + var actions = actions + + guard let strongSelf = self, !actions.items.isEmpty else { return } var reactionItems: [ReactionContextItem] = [] @@ -958,14 +960,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.namespace == Namespaces.Peer.SecretChat { reactionItems = [] } - - let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count - let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3 - if displayTextSelectionTip { - let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).start() + + var tip: ContextController.Tip? + + if tip == nil { + let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count + let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3 + if displayTextSelectionTip { + let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).start() + tip = .textSelection + } + } + + if actions.tip == nil { + actions.tip = tip } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), reactionItems: reactionItems, recognizer: recognizer, gesture: gesture, displayTextSelectionTip: displayTextSelectionTip) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), reactionItems: reactionItems, recognizer: recognizer, gesture: gesture) strongSelf.currentContextController = controller controller.reactionSelected = { [weak controller] value in guard let strongSelf = self, let message = updatedMessages.first else { @@ -2221,7 +2232,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(actions), reactionItems: [], recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(ContextController.Items(items: actions)), reactionItems: [], recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in if let controller = controller as? TooltipScreen { @@ -2298,7 +2309,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G f(.dismissWithoutContent) }))) - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(actions), reactionItems: [], recognizer: nil) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(ContextController.Items(items: actions)), reactionItems: [], recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in if let controller = controller as? TooltipScreen { @@ -2741,7 +2752,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }) }, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in @@ -2863,6 +2874,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } return strongSelf.chatDisplayNode.messageTransitionNode.isAnimatingMessage(stableId: stableId) + }, getMessageTransitionNode: { [weak self] in + guard let strongSelf = self else { + return nil + } + return strongSelf.chatDisplayNode.messageTransitionNode }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -2973,7 +2989,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! @@ -3908,7 +3924,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let accountManager = context.sharedContext.accountManager let currentThemeEmoticon = Atomic<(String?, Bool)?>(value: nil) - self.presentationDataDisposable = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, themeSettings, context.engine.themes.getChatThemes(accountManager: accountManager, onlyCached: false), themeEmoticon, self.themeEmoticonAndDarkAppearancePreviewPromise.get()).start(next: { [weak self] presentationData, themeSettings, chatThemes, themeEmoticon, themeEmoticonAndDarkAppearance in + self.presentationDataDisposable = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, themeSettings, context.engine.themes.getChatThemes(accountManager: accountManager, onlyCached: true), themeEmoticon, self.themeEmoticonAndDarkAppearancePreviewPromise.get()).start(next: { [weak self] presentationData, themeSettings, chatThemes, themeEmoticon, themeEmoticonAndDarkAppearance in if let strongSelf = self { let (themeEmoticonPreview, darkAppearancePreview) = themeEmoticonAndDarkAppearance @@ -5000,41 +5016,49 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let duration: Double = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.animationDuration : 0.18 let curve: ContainedViewLayoutTransitionCurve = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.verticalAnimationCurve : .easeInOut let controlPoints: (Float, Float, Float, Float) = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNode.verticalAnimationControlPoints : (0.5, 0.33, 0.0, 0.0) + + let shouldUseFastMessageSendAnimation = strongSelf.chatDisplayNode.shouldUseFastMessageSendAnimation strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationLayout(layout: validLayout).navigationFrame.maxY, transition: .animated(duration: duration, curve: curve), listViewTransaction: { updateSizeAndInsets, _, _, _ in + var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) let _ = options.insert(.PreferSynchronousResourceLoading) - options.remove(.AnimateInsertion) - options.insert(.RequestItemInsertionAnimations) - - let deleteItems = transition.deleteItems.map({ item in - return ListViewDeleteItem(index: item.index, directionHint: nil) - }) - - var maxInsertedItem: Int? - var insertedIndex: Int? + + var deleteItems = transition.deleteItems var insertItems: [ListViewInsertItem] = [] - for i in 0 ..< transition.insertItems.count { - let item = transition.insertItems[i] - if item.directionHint == .Down && (maxInsertedItem == nil || maxInsertedItem! < item.index) { - maxInsertedItem = item.index - } - insertedIndex = item.index - insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) - } - - var scrollToItem: ListViewScrollToItem? - if isScheduledMessages, let insertedIndex = insertedIndex { - scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Down) - } else if transition.historyView.originalView.laterId == nil { - scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Up) - } - var stationaryItemRange: (Int, Int)? - if let maxInsertedItem = maxInsertedItem { - stationaryItemRange = (maxInsertedItem + 1, Int.max) + var scrollToItem: ListViewScrollToItem? + + if shouldUseFastMessageSendAnimation { + options.remove(.AnimateInsertion) + options.insert(.RequestItemInsertionAnimations) + + deleteItems = transition.deleteItems.map({ item in + return ListViewDeleteItem(index: item.index, directionHint: nil) + }) + + var maxInsertedItem: Int? + var insertedIndex: Int? + for i in 0 ..< transition.insertItems.count { + let item = transition.insertItems[i] + if item.directionHint == .Down && (maxInsertedItem == nil || maxInsertedItem! < item.index) { + maxInsertedItem = item.index + } + insertedIndex = item.index + insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) + } + + if isScheduledMessages, let insertedIndex = insertedIndex { + scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Down) + } else if transition.historyView.originalView.laterId == nil { + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Up) + } + + if let maxInsertedItem = maxInsertedItem { + stationaryItemRange = (maxInsertedItem + 1, Int.max) + } } mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: transition.peerType, networkType: transition.networkType, animateIn: false, reason: transition.reason, flashIndicators: transition.flashIndicators), updateSizeAndInsets) @@ -5750,7 +5774,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items, reactionItems: []) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: []) contextController.dismissedForCancel = { [weak chatController] in if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds { var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] @@ -6504,7 +6528,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) - contextController.setItems(.single(contextItems), minHeight: nil) + contextController.setItems(.single(ContextController.Items(items: contextItems)), minHeight: nil) } return } else { @@ -6523,7 +6547,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }))) - contextController.setItems(.single(contextItems), minHeight: nil) + contextController.setItems(.single(ContextController.Items(items: contextItems)), minHeight: nil) return } else { @@ -7258,7 +7282,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { @@ -7353,11 +7377,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let postbox = self.context.account.postbox let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) - var activityCategory: PeerActivitySpace.Category = .global - if case let .replyThread(replyThreadMessage) = self.chatLocation { - activityCategory = .thread(makeMessageThreadId(replyThreadMessage.messageId)) + + let activitySpace: PeerActivitySpace + switch self.chatLocation { + case let .peer(peerId): + activitySpace = PeerActivitySpace(peerId: peerId, category: .global) + case let .replyThread(replyThreadMessage): + activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, category: .thread(makeMessageThreadId(replyThreadMessage.messageId))) } - self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, category: activityCategory)) + + self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: activitySpace) |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in var foundAllPeers = true var cachedResult: [(Peer, PeerInputActivity)] = [] @@ -7390,7 +7419,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] activities in if let strongSelf = self { - strongSelf.chatTitleView?.inputActivities = (peerId, activities) + let displayActivities = activities.filter({ + switch $0.1 { + case .speakingInGroupCall, .interactingWithEmoji: + return false + default: + return true + } + }) + strongSelf.chatTitleView?.inputActivities = (peerId, displayActivities) + + for activity in activities { + if case let .interactingWithEmoji(emoticon, maybeInteraction) = activity.1, let interaction = maybeInteraction { + var found = false + strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode({ itemNode in + if !found, let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode, let item = itemNode.item { + if item.message.text.strippedEmoji == emoticon { + for animation in interaction.animations { + if animation.timeOffset > 0.0 { + Queue.mainQueue().after(Double(animation.timeOffset)) { + itemNode.playAdditionalAnimation(index: animation.index) + } + } else { + itemNode.playAdditionalAnimation(index: animation.index) + } + } + found = true + } + } + }) + + if found { + let _ = strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .seeingEmojiInteraction(emoticon: emoticon), isPresent: true) + } + } + } } }) } @@ -12636,7 +12699,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if canDisplayContextMenu, let contextController = contextController { - contextController.setItems(.single(contextItems), minHeight: nil) + contextController.setItems(.single(ContextController.Items(items: contextItems)), minHeight: nil) } else { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in @@ -13405,6 +13468,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } } + controller.dismissed = { [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.tapped = nil + } + } + strongSelf.chatDisplayNode.historyNode.tapped = { [weak controller] in + controller?.dimTapped() + } strongSelf.present(controller, in: .window(.root)) strongSelf.themeSceen = controller }) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index c372ea2366..bbb88a1eaf 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -123,6 +123,7 @@ public final class ChatControllerInteraction { let copyText: (String) -> Void let displayUndo: (UndoOverlayContent) -> Void let isAnimatingMessage: (UInt32) -> Bool + var getMessageTransitionNode: () -> ChatMessageTransitionNode? let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -217,6 +218,7 @@ public final class ChatControllerInteraction { copyText: @escaping (String) -> Void, displayUndo: @escaping (UndoOverlayContent) -> Void, isAnimatingMessage: @escaping (UInt32) -> Bool, + getMessageTransitionNode: @escaping () -> ChatMessageTransitionNode?, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, @@ -297,6 +299,7 @@ public final class ChatControllerInteraction { self.copyText = copyText self.displayUndo = displayUndo self.isAnimatingMessage = isAnimatingMessage + self.getMessageTransitionNode = getMessageTransitionNode self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -353,6 +356,8 @@ public final class ChatControllerInteraction { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 1c576ff50b..20ea23a5f7 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2578,6 +2578,19 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { return false } + var hasAd = false + self.historyNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let _ = itemNode.item?.message.adAttribute { + hasAd = true + } + } + } + + if hasAd { + return false + } + switch self.historyNode.visibleContentOffset() { case let .known(value) where value < 20.0: return true @@ -2588,6 +2601,23 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + var shouldUseFastMessageSendAnimation: Bool { + var hasAd = false + self.historyNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let _ = itemNode.item?.message.adAttribute { + hasAd = true + } + } + } + + if hasAd { + return false + } + + return true + } + var shouldAllowOverscrollActions: Bool { if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 0.0 { return false diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index db6b7a20b7..a88c5f79ed 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -311,7 +311,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown @@ -360,7 +360,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist } } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId) } private extension ChatHistoryLocationInput { @@ -555,8 +555,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var nextChannelToReadDisplayName: Bool = false private var currentOverscrollExpandProgress: CGFloat = 0.0 private var freezeOverscrollControl: Bool = false + private var freezeOverscrollControlProgress: Bool = false private var feedback: HapticFeedback? var openNextChannelToRead: ((EnginePeer, TelegramEngine.NextUnreadChannelLocation) -> Void)? + private var contentInsetAnimator: DisplayLinkAnimator? private let adMessagesContext: AdMessagesHistoryContext? @@ -786,6 +788,32 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return animatedEmojiStickers } + let additionalAnimatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false) + |> map { animatedEmoji -> [String: [Int: StickerPackItem]] in + let sequence = "0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣" + var animatedEmojiStickers: [String: [Int: StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for case let item as StickerPackItem in items { + let indexKeys = item.getStringRepresentationsOfIndexKeys() + if indexKeys.count > 1, let emoji = indexKeys.first, let indexEmoji = indexKeys.last?.first { + if let strIndex = sequence.firstIndex(of: indexEmoji) { + let emoji = emoji.strippedEmoji + let index = sequence.distance(from: sequence.startIndex, to: strIndex) + if animatedEmojiStickers[emoji] != nil { + animatedEmojiStickers[emoji]![index] = item + } else { + animatedEmojiStickers[emoji] = [index: item] + } + } + } + } + default: + break + } + return animatedEmojiStickers + } + let previousHistoryAppearsCleared = Atomic(value: nil) let updatingMedia = context.account.pendingUpdateMessageManager.updatingMessageMedia @@ -869,11 +897,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.pendingUnpinnedAllMessagesPromise.get(), self.pendingRemovedMessagesPromise.get(), animatedEmojiStickers, + additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, self.currentlyPlayingMessageIdPromise.get(), adMessages - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages in + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -956,7 +985,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { reverse = reverseValue } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId) let filteredEntries = chatHistoryEntriesForView( location: chatLocation, @@ -1267,9 +1296,32 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { guard let strongSelf = self else { return } - if let nextChannelToRead = strongSelf.nextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 { - strongSelf.freezeOverscrollControl = true - strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.location) + if strongSelf.offerNextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 { + if let nextChannelToRead = strongSelf.nextChannelToRead { + strongSelf.freezeOverscrollControl = true + strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.location) + } else { + strongSelf.freezeOverscrollControlProgress = true + strongSelf.scroller.contentInset = UIEdgeInsets(top: 94.0 + 12.0, left: 0.0, bottom: 0.0, right: 0.0) + Queue.mainQueue().after(0.3, { + let animator = DisplayLinkAnimator(duration: 0.2, from: 1.0, to: 0.0, update: { rawT in + guard let strongSelf = self else { + return + } + let t = listViewAnimationCurveEaseInOut(rawT) + let value = (94.0 + 12.0) * t + strongSelf.scroller.contentInset = UIEdgeInsets(top: value, left: 0.0, bottom: 0.0, right: 0.0) + }, completion: { + guard let strongSelf = self else { + return + } + strongSelf.contentInsetAnimator = nil + strongSelf.scroller.contentInset = UIEdgeInsets() + strongSelf.freezeOverscrollControlProgress = false + }) + strongSelf.contentInsetAnimator = animator + }) + } } } @@ -1369,7 +1421,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil) } - let overscrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.insets.top), size: CGSize(width: self.bounds.width, height: 94.0)) + var overscrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.insets.top), size: CGSize(width: self.bounds.width, height: 94.0)) + if self.freezeOverscrollControlProgress { + overscrollFrame.origin.y -= max(0.0, 94.0 - expandDistance) + } + + overscrollView.frame = self.view.convert(overscrollFrame, to: self.view.superview!) let _ = overscrollView.update( transition: .immediate, @@ -1380,7 +1437,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { unreadCount: self.nextChannelToRead?.unreadCount ?? 0, location: self.nextChannelToRead?.location ?? .same, context: self.context, - expandDistance: expandDistance, + expandDistance: self.freezeOverscrollControl ? 94.0 : expandDistance, + freezeProgress: false, absoluteRect: CGRect(origin: CGPoint(x: overscrollFrame.minX, y: self.bounds.height - overscrollFrame.minY), size: overscrollFrame.size), absoluteSize: self.bounds.size, wallpaperNode: chatControllerNode.backgroundNode @@ -1388,7 +1446,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { environment: {}, containerSize: CGSize(width: self.bounds.width, height: 200.0) ) - overscrollView.frame = self.view.convert(overscrollFrame, to: self.view.superview!) } else if let overscrollView = self.overscrollView { self.overscrollView = nil overscrollView.removeFromSuperview() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index d1ecb68384..ba5d8053f6 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -22,6 +22,7 @@ import ShimmerEffect import AnimatedAvatarSetNode import AvatarNode import AdUI +import TelegramNotices private struct MessageContextMenuData { let starStatus: Bool? @@ -141,15 +142,18 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo } private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool { - if !isMessageRead { - return false - } - if message.flags.contains(.Incoming) { - return false - } guard let peer = message.peers[message.id.peerId] else { return false } + + if message.flags.contains(.Incoming) { + return false + } else { + if !isMessageRead { + return false + } + } + for media in message.media { if let _ = media as? TelegramMediaAction { return false @@ -351,9 +355,9 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState, return updated } -func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil) -> Signal<[ContextMenuItem], NoError> { +func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?, readStats: MessageReadStats? = nil) -> Signal { guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { - return .single([]) + return .single(ContextController.Items(items: [])) } if messages.count == 1, let _ = messages[0].adAttribute { @@ -420,7 +424,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } - return .single(actions) + return .single(ContextController.Items(items: actions)) } var loadStickerSaveStatus: MediaId? @@ -534,7 +538,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return transaction.getCombinedPeerReadState(messages[0].id.peerId) } - let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool), NoError> = combineLatest( + let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32), NoError> = combineLatest( loadLimits, loadStickerSaveStatusSignal, loadResourceStatusSignal, @@ -542,9 +546,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState context.account.pendingUpdateMessageManager.updatingMessageMedia |> take(1), cachedData, - readState + readState, + ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager) ) - |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool) in + |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32) in let (limitsConfiguration, appConfig) = limitsAndAppConfig var canEdit = false if !isAction { @@ -557,12 +562,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState isMessageRead = readState.isOutgoingMessageIndexRead(message.index) } - return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead) + return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips) } return dataSignal |> deliverOnMainQueue - |> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead -> [ContextMenuItem] in + |> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips -> ContextController.Items in var actions: [ContextMenuItem] = [] var isPinnedMessages = false @@ -1191,6 +1196,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controller.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction, readStats: stats), minHeight: nil, previousActionsTransition: .slide(forward: false)) }))) + subActions.append(.separator) + for peer in stats.peers { let avatarSignal = peerAvatarCompleteImage(account: context.account, peer: peer._asPeer(), size: CGSize(width: 30.0, height: 30.0)) @@ -1201,8 +1208,14 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } + var tip: ContextController.Tip? + if messageViewsPrivacyTips < 3 { + tip = .messageViewsPrivacy + let _ = ApplicationSpecificNotice.incrementMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager).start() + } + let minHeight = c.getActionsMinHeight() - c.setItems(.single(subActions), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) + c.setItems(.single(ContextController.Items(items: subActions, tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) } else { f(.default) } @@ -1210,7 +1223,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - return actions + return ContextController.Items(items: actions, tip: nil) } } @@ -1725,6 +1738,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus private let backgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + private let placeholderCalculationTextNode: ImmediateTextNode private let textNode: ImmediateTextNode private let shimmerNode: ShimmerEffectNode private let iconNode: ASImageNode @@ -1759,11 +1773,15 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor self.highlightedBackgroundNode.alpha = 0.0 + self.placeholderCalculationTextNode = ImmediateTextNode() + self.placeholderCalculationTextNode.attributedText = NSAttributedString(string: presentationData.strings.Conversation_ContextMenuSeen(11), font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.placeholderCalculationTextNode.maximumNumberOfLines = 1 + self.textNode = ImmediateTextNode() self.textNode.isAccessibilityElement = false self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = false - self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.destructiveColor) + self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) self.textNode.maximumNumberOfLines = 1 self.textNode.alpha = 0.0 @@ -1905,6 +1923,8 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - iconSize.width - 4.0, height: .greatestFiniteMagnitude)) + let placeholderTextSize = self.placeholderCalculationTextNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset - iconSize.width - 4.0, height: .greatestFiniteMagnitude)) + let combinedTextHeight = textSize.height return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in self.validLayout = (calculatedWidth: calculatedWidth, size: size) @@ -1915,7 +1935,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus let shimmerHeight: CGFloat = 8.0 - self.shimmerNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: floor((size.height - shimmerHeight) / 2.0)), size: CGSize(width: min(100.0, size.width - 40.0), height: shimmerHeight)) + self.shimmerNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: floor((size.height - shimmerHeight) / 2.0)), size: CGSize(width: placeholderTextSize.width, height: shimmerHeight)) self.shimmerNode.cornerRadius = shimmerHeight / 2.0 let shimmeringForegroundColor = self.presentationData.theme.contextMenu.itemSeparatorColor.blitOver(self.presentationData.theme.list.plainBackgroundColor, alpha: 0.9) let shimmeringColor = self.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.2) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 6447d45be0..3ec3531acd 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1458,7 +1458,7 @@ final class ChatMediaInputNode: ChatInputNode { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil) }) } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 4509b38080..41f748c93a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -168,9 +168,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private(set) var placeholderNode: StickerShimmerEffectNode private(set) var animationNode: GenericAnimatedStickerNode? private var animationSize: CGSize? - private var additionalAnimationNodes: [AnimatedStickerNode] = [] private var didSetUpAnimationNode = false private var isPlaying = false + + private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = [] + private var enqueuedAdditionalAnimations: [(Int, Double)] = [] + private var additionalAnimationsCommitTimer: SwiftSignalKit.Timer? private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? @@ -183,6 +186,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var emojiFile: TelegramMediaFile? var telegramDice: TelegramMediaDice? private let disposable = MetaDisposable() + private let disposables = DisposableSet() private var forwardInfoNode: ChatMessageForwardInfoNode? private var forwardBackgroundNode: NavigationBackgroundNode? @@ -305,7 +309,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { deinit { self.disposable.dispose() + self.disposables.dispose() self.mediaStatusDisposable.set(nil) + self.additionalAnimationsCommitTimer?.invalidate() } required init?(coder aDecoder: NSCoder) { @@ -396,6 +402,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if self.visibilityStatus != oldValue { self.updateVisibility() self.haptic?.enabled = self.visibilityStatus + + } } } @@ -513,6 +521,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) } self.updateVisibility() + + if let animationItems = item.associatedData.additionalAnimatedEmojiStickers[item.message.text.strippedEmoji] { + for (_, animationItem) in animationItems { + self.disposables.add(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: animationItem.file)).start()) + } + } } } } @@ -524,6 +538,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let animationNode = self.animationNode as? AnimatedStickerNode { let isPlaying = self.visibilityStatus && !self.forceStopAnimations + + if !isPlaying { + for decorationNode in self.additionalAnimationNodes { + if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { + decorationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak decorationNode] _ in + if let decorationNode = decorationNode { + transitionNode.remove(decorationNode: decorationNode) + } + }) + } + } + self.additionalAnimationNodes.removeAll() + } + if self.isPlaying != isPlaying { self.isPlaying = isPlaying @@ -1290,34 +1318,93 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - private func playAdditionalAnimation(_ name: String) { - let source = AnimatedStickerNodeLocalFileSource(name: name) - guard let item = self.item, let path = source.path, let animationSize = self.animationSize, let animationNode = self.animationNode, self.additionalAnimationNodes.count < 4 else { + private func startAdditionalAnimationsCommitTimer() { + guard self.additionalAnimationsCommitTimer == nil else { return } - let incoming = item.message.effectivelyIncoming(item.context.account.peerId) - - self.supernode?.view.bringSubviewToFront(self.view) - - let resource = BundleResource(name: name, path: path) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) - - let additionalAnimationNode = AnimatedStickerNode() - additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 3.0), height: Int(animationSize.height * 3.0), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) - additionalAnimationNode.completed = { [weak self, weak additionalAnimationNode] _ in - self?.additionalAnimationNodes.removeAll(where: { $0 === additionalAnimationNode }) - additionalAnimationNode?.removeFromSupernode() + let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + self?.commitEnqueuedAnimations() + self?.additionalAnimationsCommitTimer?.invalidate() + self?.additionalAnimationsCommitTimer = nil + }, queue: Queue.mainQueue()) + self.additionalAnimationsCommitTimer = timer + timer.start() + } + + private func commitEnqueuedAnimations() { + guard let item = self.item, !self.enqueuedAdditionalAnimations.isEmpty else { + return } + let textEmoji = item.message.text.strippedEmoji + + let enqueuedAnimations = self.enqueuedAdditionalAnimations + self.enqueuedAdditionalAnimations.removeAll() + + guard let startTimestamp = enqueuedAnimations.first?.1 else { + return + } + + var animations: [EmojiInteraction.Animation] = [] + 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, interaction: EmojiInteraction(animations: animations)), isPresent: true) + } + + func playAdditionalAnimation(index: Int) { + guard let item = self.item else { + return + } + + let textEmoji = item.message.text.strippedEmoji + guard let animationItems = item.associatedData.additionalAnimatedEmojiStickers[textEmoji], index < 10, let file = animationItems[index]?.file else { + return + } + let source = AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: nil) + guard let animationSize = self.animationSize, let animationNode = self.animationNode, self.additionalAnimationNodes.count < 4 else { + return + } + + if let animationNode = animationNode as? AnimatedStickerNode { + let _ = animationNode.playIfNeeded() + } + + let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId) + + let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let additionalAnimationNode = AnimatedStickerNode() + additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 2.0), height: Int(animationSize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) var animationFrame = animationNode.frame.insetBy(dx: -animationNode.frame.width, dy: -animationNode.frame.height) - .offsetBy(dx: incoming ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0) + .offsetBy(dx: incomingMessage ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0) animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0)) additionalAnimationNode.frame = animationFrame - if incoming { + if incomingMessage { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) } - self.addSubnode(additionalAnimationNode) + + guard let transitionNode = item.controllerInteraction.getMessageTransitionNode() else { + return + } + let decorationNode = transitionNode.add(decorationNode: additionalAnimationNode, itemNode: self) + additionalAnimationNode.completed = { [weak self, weak decorationNode, weak transitionNode] _ in + guard let decorationNode = decorationNode else { + return + } + self?.additionalAnimationNodes.removeAll(where: { $0 === decorationNode }) + transitionNode?.remove(decorationNode: decorationNode) + } + additionalAnimationNode.isPlayingChanged = { [weak self, weak decorationNode, weak transitionNode] isPlaying in + if !isPlaying { + guard let decorationNode = decorationNode else { + return + } + self?.additionalAnimationNodes.removeAll(where: { $0 === decorationNode }) + transitionNode?.remove(decorationNode: decorationNode) + } + } - self.additionalAnimationNodes.append(additionalAnimationNode) + self.additionalAnimationNodes.append(decorationNode) additionalAnimationNode.play() } @@ -1420,29 +1507,51 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let heart = 0x2764 let peach = 0x1F351 let coffin = 0x26B0 - let fireworks = 0x1F386 let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1) |> map { view in return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue } - - if let text = self.item?.message.text, var firstScalar = text.unicodeScalars.first { + + let text = item.message.text + if var firstScalar = text.unicodeScalars.first { var textEmoji = text.strippedEmoji + let originalTextEmoji = textEmoji if beatingHearts.contains(firstScalar.value) { textEmoji = "❤️" firstScalar = UnicodeScalar(heart)! } return .optionalAction({ - if firstScalar.value == heart { - if self.additionalAnimationNodes.count % 2 == 0 { - self.playAdditionalAnimation("TestHearts") - } else { - self.playAdditionalAnimation("TestHearts2") + if let animationItems = item.associatedData.additionalAnimatedEmojiStickers[originalTextEmoji] { + self.startAdditionalAnimationsCommitTimer() + + let timestamp = CACurrentMediaTime() + let previousAnimation = self.enqueuedAdditionalAnimations.last + + var availableAnimations = animationItems + var delay: Double = 0.0 + if availableAnimations.count > 1, let (previousIndex, _) = previousAnimation { + availableAnimations.removeValue(forKey: previousIndex) + } + if let (_, previousTimestamp) = previousAnimation { + delay = min(0.2, max(0.0, previousTimestamp + 0.2 - timestamp)) + } + if let index = availableAnimations.randomElement()?.0 { + if delay > 0.0 { + Queue.mainQueue().after(delay) { + self.enqueuedAdditionalAnimations.append((index, timestamp + delay)) + self.playAdditionalAnimation(index: index) + + if self.additionalAnimationsCommitTimer == nil { + self.startAdditionalAnimationsCommitTimer() + } + } + } else { + self.enqueuedAdditionalAnimations.append((index, timestamp)) + self.playAdditionalAnimation(index: index) + } } - } else if firstScalar.value == fireworks { - self.playAdditionalAnimation("TestFireworks") } if shouldPlay { diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index ab733459c0..b4b9e64b9d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -172,6 +172,56 @@ public final class ChatMessageTransitionNode: ASDisplayNode { case videoMessage(VideoMessage) case mediaInput(MediaInput) } + + final class DecorationItemNode: ASDisplayNode { + let itemNode: ChatMessageItemView + private let contentNode: ASDisplayNode + private let getContentAreaInScreenSpace: () -> CGRect + + private let scrollingContainer: ASDisplayNode + private let containerNode: ASDisplayNode + private let clippingNode: ASDisplayNode + + fileprivate weak var overlayController: OverlayTransitionContainerController? + + init(itemNode: ChatMessageItemView, contentNode: ASDisplayNode, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.itemNode = itemNode + self.contentNode = contentNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollingContainer = ASDisplayNode() + self.containerNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.scrollingContainer) + self.scrollingContainer.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.contentNode) + } + + func updateLayout(size: CGSize) { + self.clippingNode.frame = CGRect(origin: CGPoint(), size: size) + + let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.view) + self.containerNode.frame = absoluteRect + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + if transition.isAnimated { + assert(true) + } + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset) + transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset) + } + + func addContentOffset(offset: CGFloat) { + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset) + } + } private final class AnimatingItemNode: ASDisplayNode { let itemNode: ChatMessageItemView @@ -553,6 +603,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { private var currentPendingItem: (Int64, Source, () -> Void)? private var animatingItemNodes: [AnimatingItemNode] = [] + private var decorationItemNodes: [DecorationItemNode] = [] var hasScheduledTransitions: Bool { return self.currentPendingItem != nil @@ -585,6 +636,26 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.currentPendingItem = (correlationId, source, initiated) self.listNode.setCurrentSendAnimationCorrelationId(correlationId) } + + func add(decorationNode: ASDisplayNode, itemNode: ChatMessageItemView) -> DecorationItemNode { + let decorationItemNode = DecorationItemNode(itemNode: itemNode, contentNode: decorationNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + decorationItemNode.updateLayout(size: self.bounds.size) + self.decorationItemNodes.append(decorationItemNode) + + let overlayController = OverlayTransitionContainerController() + overlayController.displayNode.isUserInteractionEnabled = false + overlayController.displayNode.addSubnode(decorationItemNode) + decorationItemNode.overlayController = overlayController + itemNode.item?.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController) + + return decorationItemNode + } + + func remove(decorationNode: DecorationItemNode) { + self.decorationItemNodes.removeAll(where: { $0 === decorationNode }) + decorationNode.removeFromSupernode() + decorationNode.overlayController?.dismiss() + } private func beginAnimation(itemNode: ChatMessageItemView, source: Source) { var contextSourceNode: ContextExtractedContentContainingNode? @@ -646,12 +717,22 @@ public final class ChatMessageTransitionNode: ASDisplayNode { for animatingItemNode in self.animatingItemNodes { animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } + if itemNode == nil { + for decorationItemNode in self.decorationItemNodes { + decorationItemNode.addExternalOffset(offset: offset, transition: transition) + } + } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { for animatingItemNode in self.animatingItemNodes { animatingItemNode.addContentOffset(offset: offset, itemNode: itemNode) } + if itemNode == nil { + for decorationItemNode in self.decorationItemNodes { + decorationItemNode.addContentOffset(offset: offset) + } + } } func isAnimatingMessage(stableId: UInt32) -> Bool { diff --git a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift index b642e42e96..78b22afba5 100644 --- a/submodules/TelegramUI/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Sources/ChatOverscrollControl.swift @@ -669,6 +669,7 @@ final class OverscrollContentsComponent: Component { let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let expandOffset: CGFloat + let freezeProgress: Bool let absoluteRect: CGRect let absoluteSize: CGSize let wallpaperNode: WallpaperBackgroundNode? @@ -681,6 +682,7 @@ final class OverscrollContentsComponent: Component { unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, expandOffset: CGFloat, + freezeProgress: Bool, absoluteRect: CGRect, absoluteSize: CGSize, wallpaperNode: WallpaperBackgroundNode? @@ -692,6 +694,7 @@ final class OverscrollContentsComponent: Component { self.unreadCount = unreadCount self.location = location self.expandOffset = expandOffset + self.freezeProgress = freezeProgress self.absoluteRect = absoluteRect self.absoluteSize = absoluteSize self.wallpaperNode = wallpaperNode @@ -719,6 +722,9 @@ final class OverscrollContentsComponent: Component { if lhs.expandOffset != rhs.expandOffset { return false } + if lhs.freezeProgress != rhs.freezeProgress { + return false + } if lhs.absoluteRect != rhs.absoluteRect { return false } @@ -811,7 +817,14 @@ final class OverscrollContentsComponent: Component { let minBackgroundHeight: CGFloat = backgroundWidth + 5.0 let avatarInset: CGFloat = 6.0 - let isFullyExpanded = component.expandOffset >= fullHeight + let apparentExpandOffset: CGFloat + if component.freezeProgress { + apparentExpandOffset = fullHeight + } else { + apparentExpandOffset = component.expandOffset + } + + let isFullyExpanded = apparentExpandOffset >= fullHeight let isFolderMask: Bool switch component.location { @@ -821,20 +834,21 @@ final class OverscrollContentsComponent: Component { isFolderMask = false } - let expandProgress: CGFloat = max(0.1, min(1.0, component.expandOffset / fullHeight)) + let expandProgress: CGFloat = max(0.1, min(1.0, apparentExpandOffset / fullHeight)) + let trueExpandProgress: CGFloat = max(0.1, min(1.0, component.expandOffset / fullHeight)) func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat { return (1.0 - value) * from + value * to } - let backgroundHeight: CGFloat = interpolate(from: minBackgroundHeight, to: fullHeight, value: expandProgress) + let backgroundHeight: CGFloat = interpolate(from: minBackgroundHeight, to: fullHeight, value: trueExpandProgress) let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundWidth) / 2.0), y: fullHeight - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight)) - let alphaProgress: CGFloat = max(0.0, min(1.0, component.expandOffset / 10.0)) + let alphaProgress: CGFloat = max(0.0, min(1.0, apparentExpandOffset / 10.0)) let maxAvatarScale: CGFloat = 1.0 - var avatarExpandProgress: CGFloat = max(0.01, min(maxAvatarScale, component.expandOffset / fullHeight)) + var avatarExpandProgress: CGFloat = max(0.01, min(maxAvatarScale, apparentExpandOffset / fullHeight)) avatarExpandProgress *= expandProgress let avatarOffsetProgress = interpolate(from: 0.1, to: 1.0, value: avatarExpandProgress) @@ -978,6 +992,7 @@ final class ChatOverscrollControl: CombinedComponent { let location: TelegramEngine.NextUnreadChannelLocation let context: AccountContext let expandDistance: CGFloat + let freezeProgress: Bool let absoluteRect: CGRect let absoluteSize: CGSize let wallpaperNode: WallpaperBackgroundNode? @@ -990,6 +1005,7 @@ final class ChatOverscrollControl: CombinedComponent { location: TelegramEngine.NextUnreadChannelLocation, context: AccountContext, expandDistance: CGFloat, + freezeProgress: Bool, absoluteRect: CGRect, absoluteSize: CGSize, wallpaperNode: WallpaperBackgroundNode? @@ -1001,6 +1017,7 @@ final class ChatOverscrollControl: CombinedComponent { self.location = location self.context = context self.expandDistance = expandDistance + self.freezeProgress = freezeProgress self.absoluteRect = absoluteRect self.absoluteSize = absoluteSize self.wallpaperNode = wallpaperNode @@ -1028,6 +1045,9 @@ final class ChatOverscrollControl: CombinedComponent { if lhs.expandDistance != rhs.expandDistance { return false } + if lhs.freezeProgress != rhs.freezeProgress { + return false + } if lhs.absoluteRect != rhs.absoluteRect { return false } @@ -1053,6 +1073,7 @@ final class ChatOverscrollControl: CombinedComponent { unreadCount: context.component.unreadCount, location: context.component.location, expandOffset: context.component.expandDistance, + freezeProgress: context.component.freezeProgress, absoluteRect: context.component.absoluteRect, absoluteSize: context.component.absoluteSize, wallpaperNode: context.component.wallpaperNode diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 944dc4546c..aad12e7d66 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -527,6 +527,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 1756c19d0d..18fd8322f8 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -207,7 +207,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe if let message = messages.first { let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peerId), subject: .message(id: message.id, highlight: true, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single([]), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(items: [])), reactionItems: [], gesture: gesture) presentInGlobalOverlay(contextController) } else { gesture?.cancel() diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 79cdfc2bff..982249e123 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -339,6 +339,33 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) } + override func selected() { + let wasSelected = self.item?.selected ?? false + super.selected() + + if let animatedStickerNode = self.animatedStickerNode { + Queue.mainQueue().after(0.1) { + if !wasSelected { + animatedStickerNode.seekTo(.frameIndex(0)) + animatedStickerNode.play() + + let scale: CGFloat = 2.6 + animatedStickerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + animatedStickerNode.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.45) + + animatedStickerNode.completed = { [weak animatedStickerNode, weak self] _ in + guard let item = self?.item, item.selected else { + return + } + animatedStickerNode?.transform = CATransform3DIdentity + animatedStickerNode?.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + } + } + } + + } + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) @@ -372,12 +399,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - var emoticon = item.emoticon - if emoticon == "🦁" { - emoticon = "🌳" - } else if emoticon == "🔮" { - emoticon = "🎆" - } + let emoticon = item.emoticon let title = NSAttributedString(string: emoticon != nil ? "" : "❌", font: Font.regular(22.0), textColor: .black) let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -385,7 +407,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { return (itemLayout, { animated in if let strongSelf = self { strongSelf.item = item - + if updatedThemeReference || updatedWallpaper { if let themeReference = item.themeReference { strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, emoticon: true)) @@ -400,6 +422,13 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: false, selected: item.selected) } + if !item.selected && currentItem?.selected == true, let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.transform = CATransform3DIdentity + + let initialScale: CGFloat = CGFloat((animatedStickerNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) + animatedStickerNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((90.0 - textLayout.size.width) / 2.0), y: 24.0), size: textLayout.size) strongSelf.textNode.isHidden = item.emoticon != nil @@ -438,8 +467,11 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { strongSelf.animatedStickerNode = animatedStickerNode strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 96, height: 96, mode: .direct(cachePathPrefix: pathPrefix)) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) + + animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) } + animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) @@ -506,6 +538,8 @@ final class ChatThemeScreen: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + var dismissed: (() -> Void)? + var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { didSet { if self.isNodeLoaded { @@ -549,7 +583,7 @@ final class ChatThemeScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedEmoticon: self.initiallySelectedEmoticon, dismissByTapOutside: self.dismissByTapOutside) + self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedEmoticon: self.initiallySelectedEmoticon, dismissByTapOutside: self.dismissByTapOutside) self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.previewTheme = { [weak self] emoticon, dark in guard let strongSelf = self else { @@ -606,6 +640,8 @@ final class ChatThemeScreen: ViewController { }) self.controllerNode.animateOut(completion: completion) + + self.dismissed?() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -613,6 +649,10 @@ final class ChatThemeScreen: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } + + func dimTapped() { + self.controllerNode.dimTapped() + } } private func iconColors(theme: PresentationTheme) -> [String: UIColor] { @@ -635,6 +675,7 @@ private func iconColors(theme: PresentationTheme) -> [String: UIColor] { private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData + private weak var controller: ChatThemeScreen? private let dismissByTapOutside: Bool private let dimNode: ASDisplayNode @@ -648,7 +689,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega private let textNode: ImmediateTextNode private let cancelButton: HighlightableButtonNode private let switchThemeButton: HighlightTrackingButtonNode - private let animationNode: AnimationNode + private let animationContainerNode: ASDisplayNode + private var animationNode: AnimationNode private let doneButton: SolidRoundedButtonNode private let listNode: ListView @@ -681,8 +723,9 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega var dismiss: (() -> Void)? var cancel: (() -> Void)? - init(context: AccountContext, presentationData: PresentationData, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, dismissByTapOutside: Bool) { + init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, dismissByTapOutside: Bool) { self.context = context + self.controller = controller self.initiallySelectedEmoticon = initiallySelectedEmoticon self.selectedEmoticon = initiallySelectedEmoticon self.selectedEmoticonPromise = ValuePromise(initiallySelectedEmoticon) @@ -731,11 +774,14 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) self.switchThemeButton = HighlightTrackingButtonNode() + self.animationContainerNode = ASDisplayNode() + self.animationContainerNode.isUserInteractionEnabled = false + self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) self.animationNode.isUserInteractionEnabled = false self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) - self.doneButton.title = self.presentationData.strings.Conversation_Theme_Apply + self.doneButton.title = initiallySelectedEmoticon == nil ? self.presentationData.strings.Conversation_Theme_DontSetTheme : self.presentationData.strings.Conversation_Theme_Apply self.listNode = ListView() self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) @@ -745,7 +791,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.backgroundColor = nil self.isOpaque = false - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) self.addSubnode(self.dimNode) self.wrappingScrollNode.view.delegate = self @@ -761,7 +806,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.contentContainerNode.addSubnode(self.textNode) self.contentContainerNode.addSubnode(self.doneButton) - self.topContentContainerNode.addSubnode(self.animationNode) + self.topContentContainerNode.addSubnode(self.animationContainerNode) + self.animationContainerNode.addSubnode(self.animationNode) self.topContentContainerNode.addSubnode(self.switchThemeButton) self.topContentContainerNode.addSubnode(self.listNode) self.topContentContainerNode.addSubnode(self.cancelButton) @@ -784,28 +830,27 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let presentationData = strongSelf.presentationData var entries: [ThemeSettingsThemeEntry] = [] - if strongSelf.initiallySelectedEmoticon != nil { - entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, emojiFile: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) - } + entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, emojiFile: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) for theme in themes { - var emoticon = theme.emoji - if emoticon == "🦁" { - emoticon = "🌳" - } else if emoticon == "🔮" { - emoticon = "🎆" - } + let emoticon = theme.emoji entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: theme.emoji, emojiFile: animatedEmojiStickers[emoticon]?.first?.file, themeReference: .cloud(PresentationCloudTheme(theme: isDarkAppearance ? theme.darkTheme : theme.theme, resolvedWallpaper: nil, creatorAccountId: nil)), selected: selectedEmoticon == theme.emoji, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) } let action: (String?) -> Void = { [weak self] emoticon in if let strongSelf = self, strongSelf.selectedEmoticon != emoticon { - strongSelf.animateCrossfade(animateBackground: strongSelf.presentationData.theme.overallDarkAppearance, updateSunIcon: true) + strongSelf.animateCrossfade(animateIcon: false) strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance) strongSelf.selectedEmoticon = emoticon let _ = ensureThemeVisible(listNode: strongSelf.listNode, emoticon: emoticon, animated: true) - strongSelf.doneButton.title = emoticon == nil ? strongSelf.presentationData.strings.Conversation_Theme_Reset : strongSelf.presentationData.strings.Conversation_Theme_Apply + let doneButtonTitle: String + if emoticon == nil { + doneButtonTitle = strongSelf.initiallySelectedEmoticon == nil ? strongSelf.presentationData.strings.Conversation_Theme_DontSetTheme : strongSelf.presentationData.strings.Conversation_Theme_Reset + } else { + doneButtonTitle = strongSelf.presentationData.strings.Conversation_Theme_Apply + } + strongSelf.doneButton.title = doneButtonTitle } } let previousEntries = strongSelf.entries ?? [] @@ -901,11 +946,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let previousTheme = self.presentationData.theme self.presentationData = presentationData - if let effectView = self.effectNode.view as? UIVisualEffectView { - effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { @@ -916,7 +956,19 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) if self.animationNode.isPlaying { - + if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.25) { + let previousAnimationNode = self.animationNode + self.animationNode = animationNode + + animationNode.completion = { [weak previousAnimationNode] in + previousAnimationNode?.removeFromSupernode() + } + animationNode.isUserInteractionEnabled = false + animationNode.frame = previousAnimationNode.frame + previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode) + previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } else { self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) } @@ -936,8 +988,21 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.cancel?() } + func dimTapped() { + if self.selectedEmoticon == self.initiallySelectedEmoticon { + self.cancelButtonPressed() + } else { + let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in + if let strongSelf = self { + strongSelf.completion?(strongSelf.selectedEmoticon) + } + })], actionLayout: .horizontal, dismissOnOutsideTap: true) + self.present?(alertController) + } + } + @objc func switchThemePressed() { - self.animateCrossfade(animateBackground: true) + self.animateCrossfade(animateIcon: false) self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) self.animationNode.playOnce() @@ -948,16 +1013,10 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let _ = ApplicationSpecificNotice.incrementChatSpecificThemesDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() } - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if self.dismissByTapOutside, case .ended = recognizer.state { - self.cancelButtonPressed() - } - } - - private func animateCrossfade(animateBackground: Bool = true, updateSunIcon: Bool = false) { + private func animateCrossfade(animateIcon: Bool = true) { let delay: Double = 0.2 - if let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { + if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.animationNode.frame self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) @@ -966,15 +1025,19 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega }) } - if animateBackground, let snapshotView = self.backgroundNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.backgroundNode.frame - self.backgroundNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.backgroundNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: delay, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) + Queue.mainQueue().after(delay) { + if let effectView = self.effectNode.view as? UIVisualEffectView { + UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseInOut) { + effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) + } completion: { _ in + } + } + + let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3) } - + if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.contentContainerNode.frame self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) @@ -1028,33 +1091,26 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega func animateOut(completion: (() -> Void)? = nil) { self.animatedOut = true - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() - } - completion?() - } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() + self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss?() + completion?() + } }) } var passthroughHitTestImpl: ((CGPoint) -> UIView?)? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { + var presentingAlertController = false + self.controller?.forEachController({ c in + if c is AlertController { + presentingAlertController = true + } + return true + }) + + if !presentingAlertController && self.bounds.contains(point) { if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { if let result = self.passthroughHitTestImpl?(point) { return result @@ -1109,7 +1165,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let switchThemeSize = CGSize(width: 44.0, height: 44.0) let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) let cancelSize = CGSize(width: 44.0, height: 44.0) let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 312e858394..9bd9fe4cdf 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -343,7 +343,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView { stringValue = strings.Activity_UploadingVideoMessage case .choosingSticker: stringValue = strings.Activity_ChoosingSticker - case .speakingInGroupCall: + case let .seeingEmojiInteraction(emoticon): + stringValue = strings.Activity_EnjoyingAnimations(emoticon).string + case .speakingInGroupCall, .interactingWithEmoji: stringValue = "" } } else { @@ -372,10 +374,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView { state = .uploading(string, color) case .playingGame: state = .playingGame(string, color) - case .speakingInGroupCall: + case .speakingInGroupCall, .interactingWithEmoji: state = .typingText(string, color) case .choosingSticker: state = .choosingSticker(string, color) + case .seeingEmojiInteraction: + state = .choosingSticker(string, color) } } else { if let titleContent = self.titleContent { diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index ebc558cd1e..6db0d18443 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -153,6 +153,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 56d805c414..06bf5324e4 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -145,6 +145,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, 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 e3b66286b3..c2f1be5cd2 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1797,7 +1797,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let presentationData = strongSelf.presentationData let peerId = strongSelf.peerId items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { c, _ in - c.setItems(context.account.postbox.transaction { transaction -> [ContextMenuItem] in + c.setItems(context.account.postbox.transaction { transaction -> ContextController.Items in var items: [ContextMenuItem] = [] let messageIds = [message.id] @@ -1849,7 +1849,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } - return items + return ContextController.Items(items: items) }, minHeight: nil) }))) } @@ -1866,7 +1866,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }))) } - let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) }, activateMessagePinch: { _ in @@ -1931,7 +1931,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) { items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { c, f in - c.setItems(context.account.postbox.transaction { transaction -> [ContextMenuItem] in + c.setItems(context.account.postbox.transaction { transaction -> ContextController.Items in var items: [ContextMenuItem] = [] let messageIds = [message.id] @@ -1983,7 +1983,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } - return items + return ContextController.Items(items: items) }, minHeight: nil) }))) } @@ -2006,7 +2006,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD switch previewData { case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) case .instantPage: break @@ -2180,6 +2180,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, @@ -2226,7 +2228,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil) })) ] - let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) controller.presentInGlobalOverlay(contextController) } @@ -2825,7 +2827,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) strongSelf.controller?.presentInGlobalOverlay(contextController) } @@ -3451,7 +3453,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.view.endEditing(true) if let sourceNode = self.headerNode.buttonNodes[.mute]?.referenceNode { - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -3682,7 +3684,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, action: { [weak self] c, f in self?.openReport(user: false, contextController: c, backAction: { c in if let mainItemsImpl = mainItemsImpl { - c.setItems(mainItemsImpl(), minHeight: nil) + c.setItems(mainItemsImpl() |> map { ContextController.Items(items: $0) }, minHeight: nil) } }) }))) @@ -3763,7 +3765,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.view.endEditing(true) if let sourceNode = self.headerNode.buttonNodes[.more]?.referenceNode { - let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: mainItemsImpl?() ?? .single([]), reactionItems: [], gesture: gesture) + let items = mainItemsImpl?() ?? .single([]) + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -4424,7 +4427,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }))) if let contextController = contextController { - contextController.setItems(.single(items), minHeight: nil) + contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) } else { strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -4432,7 +4435,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -4520,7 +4523,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if let contextController = contextController { - contextController.setItems(.single(items), minHeight: nil) + contextController.setItems(.single(ContextController.Items(items: items)), minHeight: nil) } else { strongSelf.state = strongSelf.state.withHighlightedButton(.voiceChat) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -4528,7 +4531,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } if let sourceNode = strongSelf.headerNode.buttonNodes[.voiceChat]?.referenceNode, let controller = strongSelf.controller { - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(items), reactionItems: [], gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), reactionItems: [], gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { strongSelf.state = strongSelf.state.withHighlightedButton(nil) @@ -5601,7 +5604,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let contextController = ContextController(account: accountContext.account, presentationData: self.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node)), items: accountContextMenuItems(context: accountContext, logout: { [weak self] in self?.logoutAccount(id: id) - }), reactionItems: [], gesture: gesture) + }) |> map { ContextController.Items(items: $0) }, reactionItems: [], gesture: gesture) self.controller?.presentInGlobalOverlay(contextController) } else { gesture?.cancel() @@ -6900,7 +6903,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { }))) } - let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture) + let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(items: items)), reactionItems: [], recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b1889f81ff..1e1670fff0 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1292,6 +1292,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/UrlEscaping/Sources/UrlEscaping.swift b/submodules/UrlEscaping/Sources/UrlEscaping.swift index a4cb5d97bb..342d13cb92 100644 --- a/submodules/UrlEscaping/Sources/UrlEscaping.swift +++ b/submodules/UrlEscaping/Sources/UrlEscaping.swift @@ -45,11 +45,18 @@ public func explicitUrl(_ url: String) -> String { return url } +private let validUrlSet: CharacterSet = { + var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) + set.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!) + set.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!) + set.insert(charactersIn: ".?!@#$^&%*+=,:;'\"`<>()[]{}/\\|~ ") + return set +}() + public func urlEncodedStringFromString(_ string: String) -> String { var nsString: NSString = string as NSString if let value = nsString.removingPercentEncoding { nsString = value as NSString } - - return nsString.addingPercentEncoding(withAllowedCharacters: CharacterSet(charactersIn: "?!@#$^&%*+=,:;'\"`<>()[]{}/\\|~ ")) ?? "" + return nsString.addingPercentEncoding(withAllowedCharacters: validUrlSet) ?? "" }