Merge branch 'master' into postbox-refactoring-1

This commit is contained in:
Ali 2021-09-13 16:32:01 +04:00
commit e7e83c32f1
69 changed files with 1211 additions and 358 deletions

View File

@ -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";

View File

@ -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<NSString *, NSString *> *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;

View File

@ -22,10 +22,11 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let contactsPeerIds: Set<EnginePeer.Id>
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<EnginePeer.Id> = 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<EnginePeer.Id> = 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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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)
})))
}
}

View File

@ -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)
})
}

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?)
func setItems(_ items: Signal<ContextController.Items, NoError>, 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<ContextController.Items, NoError>
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<Bool>()
let contentReady = Promise<Bool>()
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<ContextController.Items, NoError>, 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<ContextController.Items, NoError>, 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<ContextController.Items, NoError>
private var reactionItems: [ReactionContextItem]
private let _ready = Promise<Bool>()
@ -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<ContextController.Items, NoError>, 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<ContextController.Items, NoError>, 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<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
self.items = items
if self.isNodeLoaded {
self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition)

View File

@ -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<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?) {
}
public func setItems(_ items: Signal<[ContextMenuItem], NoError>, minHeight: CGFloat?, previousActionsTransition: ContextController.PreviousActionsTransition) {
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
}
private var controllerNode: PeekControllerNode {

View File

@ -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()

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
})

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -166,6 +166,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
}, initialValues: [:], queue: queue)
}
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>) -> 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<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
if signals.count == 0 {
return single([T](), E.self)

View File

@ -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)
})
})

View File

@ -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

View File

@ -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) }

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)

View File

@ -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())

View File

@ -60,6 +60,8 @@ extension StickerPackReference {
self = .animatedEmoji
case let .inputStickerSetDice(emoticon):
self = .dice(emoticon)
case .inputStickerSetAnimatedEmojiAnimations:
self = .animatedEmojiAnimations
}
}
}

View File

@ -11,3 +11,11 @@ func managedAnimatedEmojiUpdates(postbox: Postbox, network: Network) -> Signal<V
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedAnimatedEmojiAnimationsUpdates(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .animatedEmojiAnimations, forceActualized: true)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}
}
}

View File

@ -35,11 +35,16 @@ func _internal_messageReadStats(account: Account, id: MessageId) -> Signal<Messa
var peerIds: [PeerId] = []
var missingPeerIds: [PeerId] = []
let authorId = transaction.getMessage(id)?.author?.id
for id in result {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
if peerId == account.peerId {
continue
}
if peerId == authorId {
continue
}
peerIds.append(peerId)
if transaction.getPeer(peerId) == nil {
missingPeerIds.append(peerId)

View File

@ -322,13 +322,14 @@ private class ReplyThreadHistoryContextImpl {
return
}
let fromId: Int32?
let fromIdExclusive: Int32?
let toIndex = messageIndex
if let maxReadIncomingMessageId = self.maxReadIncomingMessageIdValue {
fromId = maxReadIncomingMessageId.id + 1
fromIdExclusive = maxReadIncomingMessageId.id
} else {
fromId = nil
fromIdExclusive = nil
}
self.maxReadIncomingMessageIdValue = messageIndex.id
let account = self.account
@ -370,7 +371,7 @@ private class ReplyThreadHistoryContextImpl {
}
let inputPeer = transaction.getPeer(messageIndex.id.peerId).flatMap(apiInputPeer)
let readCount = transaction.getThreadMessageCount(peerId: messageId.peerId, threadId: makeMessageThreadId(messageId), namespace: messageId.namespace, fromId: fromId, toIndex: toIndex)
let readCount = transaction.getThreadMessageCount(peerId: messageId.peerId, threadId: makeMessageThreadId(messageId), namespace: messageId.namespace, fromIdExclusive: fromIdExclusive, toIndex: toIndex)
let topMessageId = transaction.getMessagesWithThreadId(peerId: messageId.peerId, namespace: messageId.namespace, threadId: makeMessageThreadId(messageId), from: MessageIndex.upperBound(peerId: messageId.peerId, namespace: messageId.namespace), includeFrom: false, to: MessageIndex.lowerBound(peerId: messageId.peerId, namespace: messageId.namespace), limit: 1).first?.id
return (inputPeer, topMessageId, readCount)
@ -386,7 +387,6 @@ private class ReplyThreadHistoryContextImpl {
var revalidate = false
strongSelf.maxReadIncomingMessageIdValue = messageIndex.id
var unreadCountValue = strongSelf.unreadCountValue
if let readCount = readCount {
unreadCountValue = max(0, unreadCountValue - Int(readCount))
@ -404,7 +404,12 @@ private class ReplyThreadHistoryContextImpl {
if let state = strongSelf.stateValue {
if let indices = state.holeIndices[messageIndex.id.namespace] {
let fromIdInt = Int(fromId ?? 1)
let fromIdInt: Int
if let fromIdExclusive = fromIdExclusive {
fromIdInt = Int(fromIdExclusive + 1)
} else {
fromIdInt = 1
}
let toIdInt = Int(toIndex.id.id)
if fromIdInt <= toIdInt, indices.intersects(integersIn: fromIdInt ..< toIdInt) {
revalidate = true

View File

@ -23,6 +23,9 @@ func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo,
case .animatedEmoji:
namespace = Namespaces.ItemCollection.CloudAnimatedEmoji
id = 0
case .animatedEmojiAnimations:
namespace = Namespaces.ItemCollection.CloudAnimatedEmojiAnimations
id = 0
case let .dice(emoji):
namespace = Namespaces.ItemCollection.CloudDice
id = Int64(murMurHashString32(emoji))
@ -104,6 +107,20 @@ func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference:
} else {
return (.fetching, true, nil)
}
case .animatedEmojiAnimations:
let namespace = Namespaces.ItemCollection.CloudAnimatedEmojiAnimations
let id: ItemCollectionId.Id = 0
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 {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
return (current, true, previousHash)
} else {
return (current, false, previousHash)
}
} else {
return (.fetching, true, nil)
}
}
}
|> 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
}

View File

@ -19,6 +19,8 @@ extension StickerPackReference {
return .inputStickerSetAnimatedEmoji
case let .dice(emoji):
return .inputStickerSetDice(emoticon: emoji)
case .animatedEmojiAnimations:
return .inputStickerSetAnimatedEmojiAnimations
}
}
}

View File

@ -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

View File

@ -104,7 +104,7 @@ func _internal_getChatThemes(accountManager: AccountManager<TelegramAccountManag
}
}
|> 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<TelegramAccountManagerTypes>, network: Network) -> Signal<Void, NoError> {
let poll = _internal_getChatThemes(accountManager: accountManager, network: network)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}

View File

@ -2,7 +2,6 @@ import Foundation
import Postbox
import TelegramApi
extension JSON {
private init?(_ object: Any) {
if let object = object as? JSONValue {

View File

@ -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<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
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<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Void, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<Bool, NoError> {
return accountManager.transaction { transaction -> Bool in

View File

@ -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 {

View File

@ -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
})

View File

@ -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,

View File

@ -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

View File

@ -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<PeerId> = 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<Bool?>(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()

View File

@ -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<ContextController.Items, NoError> {
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)

View File

@ -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)
})
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -527,6 +527,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, displayUndo: { _ in
}, isAnimatingMessage: { _ in
return false
}, getMessageTransitionNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -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()

View File

@ -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)

View File

@ -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 {

View File

@ -153,6 +153,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, displayUndo: { _ in
}, isAnimatingMessage: { _ in
return false
}, getMessageTransitionNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -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))

View File

@ -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)
}
}

View File

@ -1292,6 +1292,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, displayUndo: { _ in
}, isAnimatingMessage: { _ in
return false
}, getMessageTransitionNode: {
return nil
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -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) ?? ""
}