mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' into postbox-refactoring-1
This commit is contained in:
commit
e7e83c32f1
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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) }
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
||||
|
@ -60,6 +60,8 @@ extension StickerPackReference {
|
||||
self = .animatedEmoji
|
||||
case let .inputStickerSetDice(emoticon):
|
||||
self = .dice(emoticon)
|
||||
case .inputStickerSetAnimatedEmojiAnimations:
|
||||
self = .animatedEmojiAnimations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ extension StickerPackReference {
|
||||
return .inputStickerSetAnimatedEmoji
|
||||
case let .dice(emoji):
|
||||
return .inputStickerSetDice(emoticon: emoji)
|
||||
case .animatedEmojiAnimations:
|
||||
return .inputStickerSetAnimatedEmojiAnimations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
|
||||
|
||||
extension JSON {
|
||||
private init?(_ object: Any) {
|
||||
if let object = object as? JSONValue {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -527,6 +527,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, displayUndo: { _ in
|
||||
}, isAnimatingMessage: { _ in
|
||||
return false
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -153,6 +153,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}, displayUndo: { _ in
|
||||
}, isAnimatingMessage: { _ in
|
||||
return false
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1292,6 +1292,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, displayUndo: { _ in
|
||||
}, isAnimatingMessage: { _ in
|
||||
return false
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -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) ?? ""
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user