mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-04 21:41:45 +00:00
Merge commit '40456cd0691db79f621f70bf57baa7ee5fbacacb'
This commit is contained in:
commit
bfab84c9b6
@ -63,6 +63,10 @@
|
||||
09B4EE4F21A7B75D00847FA6 /* PermissionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */; };
|
||||
09B4EE5221A7CC3E00847FA6 /* SolidRoundedButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */; };
|
||||
09B4EE5621A8149C00847FA6 /* NotificationPermissionInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5521A8149C00847FA6 /* NotificationPermissionInfoItem.swift */; };
|
||||
09B4EE5E21AC626B00847FA6 /* PermissionContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */; };
|
||||
09B4EE6021AD4A0E00847FA6 /* InstantPageContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */; };
|
||||
09B4EE6221AD791600847FA6 /* InstantPageStoredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */; };
|
||||
09B4EE6421AD7B3E00847FA6 /* ItemCacheKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B4EE6321AD7B3E00847FA6 /* ItemCacheKeys.swift */; };
|
||||
09C3466D2167D63A00B76780 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C3466C2167D63A00B76780 /* Accessibility.swift */; };
|
||||
09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */; };
|
||||
09C9EA33219F79F600E90146 /* ID3Artwork.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C9EA31219F79F500E90146 /* ID3Artwork.m */; };
|
||||
@ -1135,6 +1139,10 @@
|
||||
09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionControllerNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidRoundedButtonNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE5521A8149C00847FA6 /* NotificationPermissionInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionInfoItem.swift; sourceTree = "<group>"; };
|
||||
09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionContentNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageContentNode.swift; sourceTree = "<group>"; };
|
||||
09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageStoredState.swift; sourceTree = "<group>"; };
|
||||
09B4EE6321AD7B3E00847FA6 /* ItemCacheKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCacheKeys.swift; sourceTree = "<group>"; };
|
||||
09C3466C2167D63A00B76780 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = "<group>"; };
|
||||
09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = "<group>"; };
|
||||
09C9EA31219F79F500E90146 /* ID3Artwork.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ID3Artwork.m; sourceTree = "<group>"; };
|
||||
@ -2360,6 +2368,7 @@
|
||||
children = (
|
||||
09B4EE4C21A7B73800847FA6 /* PermissionController.swift */,
|
||||
09B4EE4E21A7B75D00847FA6 /* PermissionControllerNode.swift */,
|
||||
09B4EE5D21AC626B00847FA6 /* PermissionContentNode.swift */,
|
||||
09B4EE5121A7CC3E00847FA6 /* SolidRoundedButtonNode.swift */,
|
||||
);
|
||||
name = Permissions;
|
||||
@ -3115,6 +3124,7 @@
|
||||
children = (
|
||||
D0215D371E040F53001A0B1E /* InstantPageNode.swift */,
|
||||
D0215D391E041003001A0B1E /* InstantPageLayout.swift */,
|
||||
09B4EE5F21AD4A0E00847FA6 /* InstantPageContentNode.swift */,
|
||||
D0215D3B1E041014001A0B1E /* InstantPageItem.swift */,
|
||||
D0215D3D1E041048001A0B1E /* InstantPageMedia.swift */,
|
||||
D0215D3F1E0410D9001A0B1E /* InstantPageLinkSelectionView.swift */,
|
||||
@ -3160,6 +3170,7 @@
|
||||
09619B8D21A34C0100493558 /* InstantPageScrollableNode.swift */,
|
||||
09619B9321A4ABF500493558 /* InstantPageReferenceController.swift */,
|
||||
09619B9421A4ABF600493558 /* InstantPageReferenceControllerNode.swift */,
|
||||
09B4EE6121AD791600847FA6 /* InstantPageStoredState.swift */,
|
||||
);
|
||||
name = "Instant Page";
|
||||
sourceTree = "<group>";
|
||||
@ -4556,6 +4567,7 @@
|
||||
0902838C2194AEB90067EFBD /* ImageTransparency.swift */,
|
||||
09C9EA32219F79F600E90146 /* ID3Artwork.h */,
|
||||
09C9EA31219F79F500E90146 /* ID3Artwork.m */,
|
||||
09B4EE6321AD7B3E00847FA6 /* ItemCacheKeys.swift */,
|
||||
);
|
||||
name = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -4920,6 +4932,7 @@
|
||||
D0EC6CB01EB9F58800EBF1C3 /* objects.c in Sources */,
|
||||
D0EC6CB11EB9F58800EBF1C3 /* program.c in Sources */,
|
||||
D0E412DA206A894800BEE4A2 /* SecureIdValueFormFileItem.swift in Sources */,
|
||||
09B4EE6221AD791600847FA6 /* InstantPageStoredState.swift in Sources */,
|
||||
D0EC6CB21EB9F58800EBF1C3 /* rngs.c in Sources */,
|
||||
D083491C209361DC008CFD52 /* AvatarGalleryItemFooterContentNode.swift in Sources */,
|
||||
D0EC6CB31EB9F58800EBF1C3 /* shader.c in Sources */,
|
||||
@ -5005,6 +5018,7 @@
|
||||
D0EC6CD81EB9F58800EBF1C3 /* ShakeAnimation.swift in Sources */,
|
||||
D0EC6CD91EB9F58800EBF1C3 /* ValidateAddressNameInteractive.swift in Sources */,
|
||||
D0BE30452061C09000FBE6D8 /* SecureIdAuthContentNode.swift in Sources */,
|
||||
09B4EE5E21AC626B00847FA6 /* PermissionContentNode.swift in Sources */,
|
||||
D0E412CC206A6B2300BEE4A2 /* FormControllerActionItem.swift in Sources */,
|
||||
D0471B5A1EFE70400074D609 /* BotCheckoutInfoControllerNode.swift in Sources */,
|
||||
D0EC6CDA1EB9F58800EBF1C3 /* NumericFormat.swift in Sources */,
|
||||
@ -5076,6 +5090,7 @@
|
||||
096C98BF21787C6700C211FF /* TGBridgeAudioEncoder.m in Sources */,
|
||||
D01776B81F1D6FB30044446D /* RadialProgressContentNode.swift in Sources */,
|
||||
D05D8B762195CD930064586F /* SetupTwoStepVerificationControllerNode.swift in Sources */,
|
||||
09B4EE6421AD7B3E00847FA6 /* ItemCacheKeys.swift in Sources */,
|
||||
D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */,
|
||||
D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */,
|
||||
D0EC6CFB1EB9F58800EBF1C3 /* ManagedAudioRecorder.swift in Sources */,
|
||||
@ -5645,6 +5660,7 @@
|
||||
D0FC194D201F82A000FEDBB2 /* OpenResolvedUrl.swift in Sources */,
|
||||
D0EC6E251EB9F58900EBF1C3 /* StickerPackPreviewGridItem.swift in Sources */,
|
||||
D0EC6E261EB9F58900EBF1C3 /* StickerPreviewController.swift in Sources */,
|
||||
09B4EE6021AD4A0E00847FA6 /* InstantPageContentNode.swift in Sources */,
|
||||
D0EC6E271EB9F58900EBF1C3 /* StickerPreviewControllerNode.swift in Sources */,
|
||||
D0EC6E281EB9F58900EBF1C3 /* ContactsController.swift in Sources */,
|
||||
D0EC6E291EB9F58900EBF1C3 /* ContactsControllerNode.swift in Sources */,
|
||||
|
||||
@ -110,7 +110,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
|
||||
|
||||
var updatedBackgroundImage: UIImage?
|
||||
if currentTheme != item.presentationData.theme {
|
||||
//let principalGraphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
//let principalGraphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
updatedBackgroundImage = PresentationResourcesChat.chatInfoItemBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
}
|
||||
|
||||
|
||||
@ -71,6 +71,8 @@ let ChatControllerCount = Atomic<Int32>(value: 0)
|
||||
public final class ChatController: TelegramController, KeyShortcutResponder, UIDropInteractionDelegate {
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
weak var parentController: ViewController?
|
||||
|
||||
public var peekActions: ChatControllerPeekActions = .standard
|
||||
private var didSetup3dTouch: Bool = false
|
||||
|
||||
@ -4335,6 +4337,10 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
|
||||
return nil
|
||||
}
|
||||
|
||||
func scrollToEndOfHistory() {
|
||||
self.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
|
||||
public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
self.navigateToMessage(from: nil, to: messageLocation, rememberInStack: false, animated: animated, completion: completion)
|
||||
}
|
||||
@ -4508,7 +4514,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
|
||||
return
|
||||
}
|
||||
|
||||
if case .peer(peerId) = strongSelf.chatLocation {
|
||||
if case .peer(peerId) = strongSelf.chatLocation, strongSelf.parentController == nil {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messageIds).withoutSelectionState() }) })
|
||||
strongController.dismiss()
|
||||
} else if peerId == strongSelf.account.peerId {
|
||||
@ -4566,7 +4572,11 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
|
||||
}
|
||||
}))
|
||||
|
||||
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready)
|
||||
if let parentController = strongSelf.parentController {
|
||||
(parentController.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready)
|
||||
} else {
|
||||
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, chatLocation: .peer(peerId)), animated: false, ready: ready)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -4783,35 +4793,43 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
|
||||
|
||||
private func reportPeer() {
|
||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
||||
let title: String
|
||||
var infoString: String?
|
||||
if let _ = peer as? TelegramGroup {
|
||||
title = self.presentationData.strings.Conversation_ReportSpam
|
||||
} else if let _ = peer as? TelegramChannel {
|
||||
title = self.presentationData.strings.Conversation_ReportSpam
|
||||
} else {
|
||||
title = self.presentationData.strings.Conversation_ReportSpam
|
||||
infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation
|
||||
}
|
||||
let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
if let infoString = infoString {
|
||||
items.append(ActionSheetTextItem(title: infoString))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.deleteChat(reportChatSpam: true)
|
||||
}
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
|
||||
if let peer = peer as? TelegramChannel, let username = peer.username, !username.isEmpty {
|
||||
self.present(peerReportOptionsController(account: account, subject: .peer(peer.id), present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root))
|
||||
}), in: .window(.root))
|
||||
} else {
|
||||
let title: String
|
||||
var infoString: String?
|
||||
if let _ = peer as? TelegramGroup {
|
||||
title = self.presentationData.strings.Conversation_ReportSpam
|
||||
} else if let _ = peer as? TelegramChannel {
|
||||
title = self.presentationData.strings.Conversation_ReportSpam
|
||||
} else {
|
||||
title = self.presentationData.strings.Conversation_ReportSpam
|
||||
infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation
|
||||
}
|
||||
let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme)
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
if let infoString = infoString {
|
||||
items.append(ActionSheetTextItem(title: infoString))
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.deleteChat(reportChatSpam: true)
|
||||
}
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -189,7 +189,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.historyNode = ChatHistoryListNode(account: account, chatLocation: chatLocation, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get())
|
||||
self.historyNodeContainer = ASDisplayNode()
|
||||
self.historyNodeContainer.addSubnode(self.historyNode)
|
||||
self.loadingNode = ChatLoadingNode(theme: chatPresentationInterfaceState.theme)
|
||||
self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper)
|
||||
|
||||
self.inputPanelBackgroundNode = ASDisplayNode()
|
||||
self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor
|
||||
@ -1323,7 +1323,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
if let restrictionText = restrictionText {
|
||||
if self.restrictedNode == nil {
|
||||
let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme)
|
||||
let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme, chatWallpaper: chatPresentationInterfaceState.chatWallpaper)
|
||||
self.historyNodeContainer.supernode?.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer)
|
||||
self.restrictedNode = restrictedNode
|
||||
}
|
||||
|
||||
@ -30,7 +30,8 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
|
||||
self.currentTheme = interfaceState.theme
|
||||
self.currentStrings = interfaceState.strings
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
|
||||
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: serviceColor.primaryText)
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 6.0, left: 10.0, bottom: 6.0, right: 10.0)
|
||||
@ -100,9 +101,11 @@ private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNode
|
||||
titleString = interfaceState.strings.Conversation_EncryptedPlaceholderTitleOutgoing(title).0
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
|
||||
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EncryptedDescriptionTitle, font: messageFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText)
|
||||
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EncryptedDescriptionTitle, font: messageFont, textColor: serviceColor.primaryText)
|
||||
|
||||
let strings: [String] = [
|
||||
interfaceState.strings.Conversation_EncryptedDescription1,
|
||||
@ -111,7 +114,7 @@ private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNode
|
||||
interfaceState.strings.Conversation_EncryptedDescription4
|
||||
]
|
||||
|
||||
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) }
|
||||
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) }
|
||||
|
||||
let lockIcon = PresentationResourcesChat.chatEmptyItemLockIcon(interfaceState.theme)
|
||||
|
||||
@ -210,10 +213,12 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
|
||||
self.currentTheme = interfaceState.theme
|
||||
self.currentStrings = interfaceState.strings
|
||||
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
|
||||
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: serviceColor.primaryText)
|
||||
|
||||
let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title
|
||||
self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText)
|
||||
|
||||
let strings: [String] = [
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description1,
|
||||
@ -222,7 +227,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
|
||||
interfaceState.strings.Conversation_ClousStorageInfo_Description4
|
||||
]
|
||||
|
||||
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: interfaceState.theme.chat.serviceMessage.serviceMessagePrimaryTextColor) }
|
||||
let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) }
|
||||
|
||||
for i in 0 ..< lines.count {
|
||||
if i >= self.lineNodes.count {
|
||||
@ -324,7 +329,8 @@ final class ChatEmptyNode: ASDisplayNode {
|
||||
self.currentTheme = interfaceState.theme
|
||||
self.currentStrings = interfaceState.strings
|
||||
|
||||
self.backgroundNode.image = PresentationResourcesChat.chatEmptyItemBackgroundImage(interfaceState.theme)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
|
||||
self.backgroundNode.image = graphics.chatEmptyItemBackgroundImage
|
||||
}
|
||||
|
||||
let contentType: ChatEmptyNodeContentType
|
||||
|
||||
@ -25,7 +25,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
||||
groupBucket.removeAll()
|
||||
}
|
||||
if view.tagMask == nil {
|
||||
entries.append(.HoleEntry(hole, presentationData.theme.theme, presentationData.strings))
|
||||
entries.append(.HoleEntry(hole, presentationData))
|
||||
}
|
||||
case let .MessageEntry(message, read, _, monthLocation):
|
||||
var isAdmin = false
|
||||
|
||||
@ -24,7 +24,7 @@ public enum ChatHistoryMessageSelection: Equatable {
|
||||
}
|
||||
|
||||
enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
case HoleEntry(MessageHistoryHole, PresentationTheme, PresentationStrings)
|
||||
case HoleEntry(MessageHistoryHole, ChatPresentationData)
|
||||
case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryMonthLocation?, ChatHistoryMessageSelection, Bool)
|
||||
case MessageGroupEntry(MessageGroupInfo, [(Message, Bool, ChatHistoryMessageSelection, Bool)], ChatPresentationData)
|
||||
case UnreadEntry(MessageIndex, ChatPresentationData)
|
||||
@ -33,7 +33,7 @@ enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
|
||||
var stableId: UInt64 {
|
||||
switch self {
|
||||
case let .HoleEntry(hole, _, _):
|
||||
case let .HoleEntry(hole, _):
|
||||
return UInt64(hole.stableId) | ((UInt64(1) << 40))
|
||||
case let .MessageEntry(message, _, _, _, _, _):
|
||||
return UInt64(message.stableId) | ((UInt64(2) << 40))
|
||||
@ -50,7 +50,7 @@ enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
|
||||
var index: MessageIndex {
|
||||
switch self {
|
||||
case let .HoleEntry(hole, _, _):
|
||||
case let .HoleEntry(hole, _):
|
||||
return hole.maxIndex
|
||||
case let .MessageEntry(message, _, _, _, _, _):
|
||||
return MessageIndex(message)
|
||||
@ -67,8 +67,8 @@ enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
|
||||
static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .HoleEntry(lhsHole, lhsTheme, lhsStrings):
|
||||
if case let .HoleEntry(rhsHole, rhsTheme, rhsStrings) = rhs, lhsHole == rhsHole, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
|
||||
case let .HoleEntry(lhsHole, lhsPresentationData):
|
||||
if case let .HoleEntry(rhsHole, rhsPresentationData) = rhs, lhsHole == rhsHole, lhsPresentationData === rhsPresentationData {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
||||
@ -166,11 +166,11 @@ private func mappedInsertEntries(account: Account, chatLocation: ChatLocation, a
|
||||
item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search)
|
||||
}
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
||||
case let .HoleEntry(_, theme, strings):
|
||||
case let .HoleEntry(_, presentationData):
|
||||
let item: ListViewItem
|
||||
switch mode {
|
||||
case .bubbles:
|
||||
item = ChatHoleItem(index: entry.entry.index, theme: theme, strings: strings)
|
||||
item = ChatHoleItem(index: entry.entry.index, presentationData: presentationData)
|
||||
case .list:
|
||||
item = ListMessageHoleItem()
|
||||
}
|
||||
@ -209,11 +209,11 @@ private func mappedUpdateEntries(account: Account, chatLocation: ChatLocation, a
|
||||
item = ListMessageItem(theme: presentationData.theme.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, account: account, chatLocation: chatLocation, controllerInteraction: controllerInteraction, message: messages[0].0, selection: .none, displayHeader: search)
|
||||
}
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
||||
case let .HoleEntry(_, theme, strings):
|
||||
case let .HoleEntry(_, presentationData):
|
||||
let item: ListViewItem
|
||||
switch mode {
|
||||
case .bubbles:
|
||||
item = ChatHoleItem(index: entry.entry.index, theme: theme, strings: strings)
|
||||
item = ChatHoleItem(index: entry.entry.index, presentationData: presentationData)
|
||||
case .list:
|
||||
item = ListMessageHoleItem()
|
||||
}
|
||||
|
||||
@ -9,14 +9,12 @@ private let titleFont = UIFont.systemFont(ofSize: 13.0)
|
||||
|
||||
class ChatHoleItem: ListViewItem {
|
||||
let index: MessageIndex
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let presentationData: ChatPresentationData
|
||||
//let header: ChatMessageDateHeader
|
||||
|
||||
init(index: MessageIndex, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
init(index: MessageIndex, presentationData: ChatPresentationData) {
|
||||
self.index = index
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.presentationData = presentationData
|
||||
//self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
@ -81,11 +79,14 @@ class ChatHoleItemNode: ListViewItemNode {
|
||||
let currentItem = self.item
|
||||
return { item, params, dateAtBottom in
|
||||
var updatedBackground: UIImage?
|
||||
if item.theme !== currentItem?.theme {
|
||||
updatedBackground = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme)
|
||||
if item.presentationData.theme !== currentItem?.presentationData.theme {
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
updatedBackground = graphics.chatServiceBubbleFillImage
|
||||
}
|
||||
|
||||
let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Channel_NotificationLoading, font: titleFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
|
||||
let (size, apply) = labelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Channel_NotificationLoading, font: titleFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0)
|
||||
|
||||
|
||||
@ -375,7 +375,11 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
|
||||
})
|
||||
}
|
||||
|
||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), animated: animated, completion: { [weak self] in
|
||||
var scrollToEndIfExists = false
|
||||
if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass {
|
||||
scrollToEndIfExists = true
|
||||
}
|
||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(peerId), scrollToEndIfExists: scrollToEndIfExists, animated: animated, completion: { [weak self] in
|
||||
self?.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
|
||||
})
|
||||
}
|
||||
@ -385,7 +389,11 @@ public class ChatListController: TelegramController, KeyShortcutResponder, UIVie
|
||||
self.chatListDisplayNode.chatListNode.groupSelected = { [weak self] groupId in
|
||||
if let strongSelf = self {
|
||||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .group(groupId))
|
||||
var scrollToEndIfExists = false
|
||||
if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass {
|
||||
scrollToEndIfExists = true
|
||||
}
|
||||
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .group(groupId), scrollToEndIfExists: scrollToEndIfExists)
|
||||
strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
|
||||
final class ChatLoadingNode: ASDisplayNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let activityIndicator: ActivityIndicator
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper) {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.image = PresentationResourcesChat.chatLoadingIndicatorBackgroundImage(theme)
|
||||
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.serviceMessage.serviceMessagePrimaryTextColor, 22.0, 2.0, false), speed: .regular)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(theme, wallpaper: chatWallpaper)
|
||||
self.backgroundNode.image = graphics.chatLoadingIndicatorBackgroundImage
|
||||
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: chatWallpaper)
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(serviceColor.primaryText, 22.0, 2.0, false), speed: .regular)
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, P
|
||||
return result
|
||||
}
|
||||
|
||||
private func attributedServiceMessageString(theme: PresentationTheme, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
return universalServiceMessageString(theme: theme, strings: strings, message: message, accountPeerId: accountPeerId)
|
||||
}
|
||||
|
||||
@ -30,12 +30,15 @@ func plainServiceMessageString(strings: PresentationStrings, message: Message, a
|
||||
return universalServiceMessageString(theme: nil, strings: strings, message: message, accountPeerId: accountPeerId)?.string
|
||||
}
|
||||
|
||||
private func universalServiceMessageString(theme: PresentationTheme?, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
private func universalServiceMessageString(theme: ChatPresentationThemeData?, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? {
|
||||
var attributedString: NSAttributedString?
|
||||
|
||||
let theme = theme?.chat.serviceMessage
|
||||
|
||||
let primaryTextColor = theme?.serviceMessagePrimaryTextColor ?? UIColor.black
|
||||
let primaryTextColor: UIColor
|
||||
if let theme = theme {
|
||||
primaryTextColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper).primaryText
|
||||
} else {
|
||||
primaryTextColor = .black
|
||||
}
|
||||
|
||||
let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [:])
|
||||
|
||||
@ -507,7 +510,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
||||
|
||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme.theme, strings: item.presentationData.strings, message: item.message, accountPeerId: item.account.peerId)
|
||||
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, message: item.message, accountPeerId: item.account.peerId)
|
||||
|
||||
var image: TelegramMediaImage?
|
||||
for media in item.message.media {
|
||||
@ -549,7 +552,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
|
||||
let backgroundApply = backgroundLayout(item.presentationData.theme.theme.chat.serviceMessage.serviceMessageFillColor, labelRects, 10.0, 10.0, 0.0)
|
||||
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0)
|
||||
|
||||
var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0)
|
||||
let layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)
|
||||
@ -635,7 +639,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.theme.chat.serviceMessage.serviceMessageLinkHighlightColor)
|
||||
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
linkHighlightingNode = LinkHighlightingNode(color: serviceColor.linkHighlight)
|
||||
linkHighlightingNode.inset = 2.5
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode)
|
||||
|
||||
@ -780,7 +780,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
forwardSource = forwardInfo.author
|
||||
forwardAuthorSignature = nil
|
||||
}
|
||||
let sizeAndApply = forwardInfoLayout(item.presentationData.theme.theme, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
let sizeAndApply = forwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
forwardInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
|
||||
|
||||
forwardInfoOriginY = headerSize.height
|
||||
@ -794,7 +794,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
} else {
|
||||
headerSize.height += 2.0
|
||||
}
|
||||
let sizeAndApply = replyInfoLayout(item.presentationData.theme.theme, item.presentationData.strings, item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
let sizeAndApply = replyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() })
|
||||
|
||||
replyInfoOriginY = headerSize.height
|
||||
@ -1101,7 +1101,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets)
|
||||
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
|
||||
var updatedMergedTop = mergedBottom
|
||||
var updatedMergedBottom = mergedTop
|
||||
@ -1903,7 +1903,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
if self.highlightedState != highlighted {
|
||||
self.highlightedState = highlighted
|
||||
if let backgroundType = self.backgroundType {
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
|
||||
if highlighted {
|
||||
self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics, transition: .immediate)
|
||||
|
||||
@ -153,7 +153,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
let themeUpdated = theme != currentTheme || type != currentType
|
||||
|
||||
let graphics = PresentationResourcesChat.principalGraphics(theme.theme, wallpaper: !theme.wallpaper.isEmpty)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(theme.theme, wallpaper: theme.wallpaper)
|
||||
|
||||
switch type {
|
||||
case .BubbleIncoming:
|
||||
@ -201,7 +201,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
impressionImage = graphics.mediaImpressionIcon
|
||||
}
|
||||
case .FreeIncoming:
|
||||
dateColor = theme.theme.chat.serviceMessage.serviceMessagePrimaryTextColor
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
dateColor = serviceColor.primaryText
|
||||
backgroundImage = graphics.dateAndStatusFreeBackground
|
||||
leftInset = 0.0
|
||||
loadedCheckFullImage = graphics.checkFreeFullImage
|
||||
@ -212,7 +213,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
impressionImage = graphics.freeImpressionIcon
|
||||
}
|
||||
case let .FreeOutgoing(status):
|
||||
dateColor = theme.theme.chat.serviceMessage.serviceMessagePrimaryTextColor
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
dateColor = serviceColor.primaryText
|
||||
outgoingStatus = status
|
||||
backgroundImage = graphics.dateAndStatusFreeBackground
|
||||
leftInset = 0.0
|
||||
|
||||
@ -113,7 +113,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
let graphics = PresentationResourcesChat.principalGraphics(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
|
||||
self.backgroundNode.image = graphics.dateStaticBackground
|
||||
self.stickBackgroundNode.image = graphics.dateFloatingBackground
|
||||
@ -169,7 +169,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: ChatPresentationData) {
|
||||
let graphics = PresentationResourcesChat.principalGraphics(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
|
||||
self.backgroundNode.image = graphics.dateStaticBackground
|
||||
self.stickBackgroundNode.image = graphics.dateFloatingBackground
|
||||
|
||||
@ -18,7 +18,7 @@ class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
super.init()
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer, _ authorName: String?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) {
|
||||
class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ theme: ChatPresentationThemeData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer, _ authorName: String?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) {
|
||||
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
|
||||
|
||||
return { theme, strings, type, peer, authorName, constrainedSize in
|
||||
@ -34,10 +34,11 @@ class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
|
||||
switch type {
|
||||
case let .bubble(incoming):
|
||||
titleColor = incoming ? theme.chat.bubble.incomingAccentTextColor : theme.chat.bubble.outgoingAccentTextColor
|
||||
titleColor = incoming ? theme.theme.chat.bubble.incomingAccentTextColor : theme.theme.chat.bubble.outgoingAccentTextColor
|
||||
completeSourceString = strings.Message_ForwardedMessage(peerString)
|
||||
case .standalone:
|
||||
titleColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
titleColor = serviceColor.primaryText
|
||||
completeSourceString = strings.Message_ForwardedMessageShort(peerString)
|
||||
}
|
||||
|
||||
|
||||
@ -141,14 +141,16 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
for attribute in item.message.attributes {
|
||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||
let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - videoLayout.contentSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData.theme.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
if let currentReplyBackgroundNode = currentReplyBackgroundNode {
|
||||
updatedReplyBackgroundNode = currentReplyBackgroundNode
|
||||
} else {
|
||||
updatedReplyBackgroundNode = ASImageNode()
|
||||
}
|
||||
replyBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.presentationData.theme.theme)
|
||||
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
replyBackgroundImage = graphics.chatServiceBubbleFillImage
|
||||
break
|
||||
} else if let attribute = attribute as? InlineBotMessageAttribute {
|
||||
if let peerId = attribute.peerId, let bot = item.message.peers[peerId] as? TelegramUser {
|
||||
@ -184,7 +186,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
forwardAuthorSignature = nil
|
||||
}
|
||||
let availableWidth = max(60.0, availableContentWidth - videoLayout.contentSize.width + 6.0)
|
||||
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData.theme.theme, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData.theme, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
if let currentForwardBackgroundNode = currentForwardBackgroundNode {
|
||||
updatedForwardBackgroundNode = currentForwardBackgroundNode
|
||||
@ -192,7 +194,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
updatedForwardBackgroundNode = ASImageNode()
|
||||
}
|
||||
|
||||
forwardBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.presentationData.theme.theme)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
forwardBackgroundImage = graphics.chatServiceBubbleFillImage
|
||||
}
|
||||
|
||||
var maxContentWidth = videoLayout.contentSize.width
|
||||
|
||||
@ -365,7 +365,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if hasThumbnail {
|
||||
fileIconImage = nil
|
||||
} else {
|
||||
let principalGraphics = PresentationResourcesChat.principalGraphics(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
||||
let principalGraphics = PresentationResourcesChat.principalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
|
||||
fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing
|
||||
}
|
||||
|
||||
@ -312,8 +312,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
let durationFillColor: UIColor
|
||||
switch statusDisplayType {
|
||||
case .free:
|
||||
durationTextColor = theme.theme.chat.serviceMessage.serviceMessagePrimaryTextColor
|
||||
durationFillColor = theme.theme.chat.serviceMessage.serviceMessageFillColor
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
durationTextColor = serviceColor.primaryText
|
||||
durationFillColor = serviceColor.fill
|
||||
case .bubble:
|
||||
durationFillColor = .clear
|
||||
if item.message.effectivelyIncoming(item.account.peerId) {
|
||||
|
||||
@ -39,7 +39,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
self.contentNode.addSubnode(self.lineNode)
|
||||
}
|
||||
|
||||
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: PresentationTheme, _ strings: PresentationStrings, _ account: Account, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) {
|
||||
class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: ChatPresentationThemeData, _ strings: PresentationStrings, _ account: Account, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) {
|
||||
|
||||
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
|
||||
@ -50,23 +50,24 @@ class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
let titleString = message.author?.displayTitle(strings: strings) ?? strings.User_DeletedAccount
|
||||
let (textString, isMedia) = descriptionStringForMessage(message, strings: strings, accountPeerId: account.peerId)
|
||||
|
||||
let placeholderColor: UIColor = message.effectivelyIncoming(account.peerId) ? theme.chat.bubble.incomingMediaPlaceholderColor : theme.chat.bubble.outgoingMediaPlaceholderColor
|
||||
let placeholderColor: UIColor = message.effectivelyIncoming(account.peerId) ? theme.theme.chat.bubble.incomingMediaPlaceholderColor : theme.theme.chat.bubble.outgoingMediaPlaceholderColor
|
||||
let titleColor: UIColor
|
||||
let lineImage: UIImage?
|
||||
let textColor: UIColor
|
||||
|
||||
switch type {
|
||||
case let .bubble(incoming):
|
||||
titleColor = incoming ? theme.chat.bubble.incomingAccentTextColor : theme.chat.bubble.outgoingAccentTextColor
|
||||
lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme)
|
||||
titleColor = incoming ? theme.theme.chat.bubble.incomingAccentTextColor : theme.theme.chat.bubble.outgoingAccentTextColor
|
||||
lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(theme.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(theme.theme)
|
||||
if isMedia {
|
||||
textColor = incoming ? theme.chat.bubble.incomingSecondaryTextColor : theme.chat.bubble.outgoingSecondaryTextColor
|
||||
textColor = incoming ? theme.theme.chat.bubble.incomingSecondaryTextColor : theme.theme.chat.bubble.outgoingSecondaryTextColor
|
||||
} else {
|
||||
textColor = incoming ? theme.chat.bubble.incomingPrimaryTextColor : theme.chat.bubble.outgoingPrimaryTextColor
|
||||
textColor = incoming ? theme.theme.chat.bubble.incomingPrimaryTextColor : theme.theme.chat.bubble.outgoingPrimaryTextColor
|
||||
}
|
||||
case .standalone:
|
||||
titleColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor
|
||||
lineImage = PresentationResourcesChat.chatServiceVerticalLineImage(theme)
|
||||
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
||||
titleColor = serviceColor.primaryText
|
||||
lineImage = PresentationResourcesChat.chatServiceVerticalLineImage(theme.theme)
|
||||
textColor = titleColor
|
||||
}
|
||||
|
||||
|
||||
@ -234,14 +234,16 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
for attribute in item.message.attributes {
|
||||
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
||||
let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData.theme.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
replyInfoApply = makeReplyInfoLayout(item.presentationData.theme, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
if let currentReplyBackgroundNode = currentReplyBackgroundNode {
|
||||
updatedReplyBackgroundNode = currentReplyBackgroundNode
|
||||
} else {
|
||||
updatedReplyBackgroundNode = ASImageNode()
|
||||
}
|
||||
replyBackgroundImage = PresentationResourcesChat.chatFreeformContentAdditionalInfoBackgroundImage(item.presentationData.theme.theme)
|
||||
|
||||
let graphics = PresentationResourcesChat.principalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
||||
replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,6 +202,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
switch type {
|
||||
case .twitter, .instagram:
|
||||
entityTypes.insert(.mention)
|
||||
entityTypes.insert(.hashtag)
|
||||
entityTypes.insert(.external)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -328,6 +330,21 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
break
|
||||
}
|
||||
}
|
||||
case let .hashtag(_, value):
|
||||
if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
|
||||
var hashtag = value
|
||||
if hashtag.hasPrefix("#") {
|
||||
hashtag = String(hashtag[hashtag.index(after: hashtag.startIndex)...])
|
||||
}
|
||||
switch websiteType(of: content) {
|
||||
case .twitter:
|
||||
return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false)
|
||||
case .instagram:
|
||||
return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
return result
|
||||
}
|
||||
|
||||
@ -32,6 +32,14 @@ extension TelegramWallpaper {
|
||||
return true
|
||||
}
|
||||
}
|
||||
var isBuiltin: Bool {
|
||||
switch self {
|
||||
case .builtin:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatPresentationThemeData: Equatable {
|
||||
|
||||
@ -91,8 +91,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
||||
self.loadingNode = ChatLoadingNode(theme: self.presentationData.theme)
|
||||
self.emptyNode = ChatRecentActionsEmptyNode(theme: self.presentationData.theme)
|
||||
self.loadingNode = ChatLoadingNode(theme: self.presentationData.theme, chatWallpaper: self.presentationData.chatWallpaper)
|
||||
self.emptyNode = ChatRecentActionsEmptyNode(theme: self.presentationData.theme, chatWallpaper: self.presentationData.chatWallpaper)
|
||||
self.emptyNode.alpha = 0.0
|
||||
|
||||
self.state = ChatRecentActionsControllerState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.fontSize)
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
|
||||
private let titleFont = Font.medium(16.0)
|
||||
private let textFont = Font.regular(15.0)
|
||||
|
||||
final class ChatRecentActionsEmptyNode: ASDisplayNode {
|
||||
private var theme: PresentationTheme
|
||||
private var chatWallpaper: TelegramWallpaper
|
||||
|
||||
private let backgroundNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
@ -17,8 +19,9 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode {
|
||||
private var title: String = ""
|
||||
private var text: String = ""
|
||||
|
||||
init(theme: PresentationTheme) {
|
||||
init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper) {
|
||||
self.theme = theme
|
||||
self.chatWallpaper = chatWallpaper
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
@ -31,7 +34,8 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundNode.image = PresentationResourcesChat.chatEmptyItemBackgroundImage(self.theme)
|
||||
let graphics = PresentationResourcesChat.principalGraphics(theme, wallpaper: chatWallpaper)
|
||||
self.backgroundNode.image = graphics.chatEmptyItemBackgroundImage
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
@ -48,9 +52,11 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: titleFont, textColor: self.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
let serviceColor = serviceMessageColorComponents(theme: self.theme, wallpaper: self.chatWallpaper)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: titleFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
let spacing: CGFloat = titleLayout.size.height.isZero ? 0.0 : 5.0
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.text, font: textFont, textColor: self.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.text, font: textFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + insets.left + insets.right, height: insets.top + insets.bottom + titleLayout.size.height + spacing + textLayout.size.height)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
|
||||
@ -413,7 +413,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
|
||||
var commonHeader: ListViewItemHeader?
|
||||
switch presentation {
|
||||
case .orderedByPresence:
|
||||
commonHeader = ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
||||
commonHeader = nil //ChatListSearchItemHeader(type: .contacts, theme: theme, strings: strings, actionTitle: nil, action: nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -568,7 +568,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)>
|
||||
|
||||
private let authorizationNode: PermissionControllerNode
|
||||
private var authorizationNode: PermissionContentNode?
|
||||
|
||||
init(account: Account, presentation: ContactListPresentation, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil) {
|
||||
self.account = account
|
||||
@ -582,8 +582,7 @@ final class ContactListNode: ASDisplayNode {
|
||||
|
||||
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.nameSortOrder, self.presentationData.nameDisplayOrder, self.presentationData.disableAnimations))
|
||||
|
||||
self.authorizationNode = PermissionControllerNode(theme: defaultLightAuthorizationTheme, strings: self.presentationData.strings)
|
||||
self.authorizationNode.updateData(subject: .contacts, currentStatus: .denied)
|
||||
//self.authorizationNode = //PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
|
||||
super.init()
|
||||
|
||||
@ -814,12 +813,12 @@ final class ContactListNode: ASDisplayNode {
|
||||
fixSearchableListNodeScrolling(strongSelf.listNode)
|
||||
}
|
||||
|
||||
self.authorizationNode.allow = { [weak self] in
|
||||
self?.account.telegramApplicationContext.applicationBindings.openSettings()
|
||||
}
|
||||
self.authorizationNode.openPrivacyPolicy = { [weak self] in
|
||||
self?.openPrivacyPolicy?()
|
||||
}
|
||||
// self.authorizationNode.allow = { [weak self] in
|
||||
// self?.account.telegramApplicationContext.applicationBindings.openSettings()
|
||||
// }
|
||||
// self.authorizationNode.openPrivacyPolicy = { [weak self] in
|
||||
// self?.openPrivacyPolicy?()
|
||||
// }
|
||||
|
||||
self.enableUpdates = true
|
||||
}
|
||||
@ -870,9 +869,9 @@ final class ContactListNode: ASDisplayNode {
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
let sublayout = layout.addedInsets(insets: UIEdgeInsetsMake(0.0, 0.0, 40.0, 0.0))
|
||||
transition.updateFrame(node: self.authorizationNode, frame: self.bounds)
|
||||
self.authorizationNode.containerLayoutUpdated(sublayout, navigationBarHeight: 0.0, transition: transition)
|
||||
//let sublayout = layout.addedInsets(insets: UIEdgeInsetsMake(0.0, 0.0, 40.0, 0.0))
|
||||
//transition.updateFrame(node: self.authorizationNode, frame: self.bounds)
|
||||
//self.authorizationNode.containerLayoutUpdated(sublayout, navigationBarHeight: 0.0, transition: transition)
|
||||
|
||||
if !self.hasValidLayout {
|
||||
self.hasValidLayout = true
|
||||
|
||||
@ -65,7 +65,8 @@ public class ContactsController: ViewController {
|
||||
self.authorizationDisposable = (DeviceAccess.authorizationStatus(account: account, subject: .contacts)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
strongSelf.tabBarItem.badgeValue = status != .allowed ? "!" : nil
|
||||
strongSelf.tabBarItem.badgeValue = nil
|
||||
//strongSelf.tabBarItem.badgeValue = status != .allowed ? "!" : nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -26,6 +26,8 @@ private var telegramUIDeclaredEncodables: Void = {
|
||||
declareEncodable(CachedChannelAdminIds.self, f: { CachedChannelAdminIds(decoder: $0) })
|
||||
declareEncodable(StickerSettings.self, f: { StickerSettings(decoder: $0) })
|
||||
declareEncodable(InstantPagePresentationSettings.self, f: { InstantPagePresentationSettings(decoder: $0) })
|
||||
declareEncodable(InstantPageStoredState.self, f: { InstantPageStoredState(decoder: $0) })
|
||||
declareEncodable(InstantPageStoredDetailsState.self, f: { InstantPageStoredDetailsState(decoder: $0) })
|
||||
declareEncodable(WatchPresetSettings.self, f: { WatchPresetSettings(decoder: $0) })
|
||||
return
|
||||
}()
|
||||
|
||||
@ -190,14 +190,10 @@ private let bubble = PresentationThemeChatBubble(
|
||||
)
|
||||
|
||||
private let serviceMessage = PresentationThemeServiceMessage(
|
||||
serviceMessageFillColor: UIColor(rgb: 0x18222D, alpha: 1.0),
|
||||
serviceMessagePrimaryTextColor: UIColor(rgb: 0xffffff),
|
||||
serviceMessageLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.12),
|
||||
components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x18222D, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), dateFillStatic: UIColor(rgb: 0x18222D, alpha: 1.0), dateFillFloating: UIColor(rgb: 0x18222D, alpha: 0.2)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x18222D, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), dateFillStatic: UIColor(rgb: 0x18222D, alpha: 1.0), dateFillFloating: UIColor(rgb: 0x18222D, alpha: 0.2))),
|
||||
unreadBarFillColor: UIColor(rgb: 0x213040),
|
||||
unreadBarStrokeColor: UIColor(rgb: 0x213040),
|
||||
unreadBarTextColor: UIColor(rgb: 0xffffff),
|
||||
dateFillStaticColor: UIColor(rgb: 0x18222D, alpha: 1.0),
|
||||
dateFillFloatingColor: UIColor(rgb: 0x18222D, alpha: 0.2),
|
||||
dateTextColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
|
||||
|
||||
@ -190,14 +190,10 @@ private let bubble = PresentationThemeChatBubble(
|
||||
)
|
||||
|
||||
private let serviceMessage = PresentationThemeServiceMessage(
|
||||
serviceMessageFillColor: UIColor(rgb: 0x1f1f1f, alpha: 1.0),
|
||||
serviceMessagePrimaryTextColor: UIColor(rgb: 0xffffff),
|
||||
serviceMessageLinkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.12), //!!!
|
||||
components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x1f1f1f, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), dateFillStatic: UIColor(rgb: 0x1f1f1f, alpha: 1.0), dateFillFloating: UIColor(rgb: 0xffffff, alpha: 0.2)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x1f1f1f, alpha: 1.0), primaryText: UIColor(rgb: 0xffffff), linkHighlight: UIColor(rgb: 0xffffff, alpha: 0.12), dateFillStatic: UIColor(rgb: 0x1f1f1f, alpha: 1.0), dateFillFloating: UIColor(rgb: 0xffffff, alpha: 0.2))),
|
||||
unreadBarFillColor: UIColor(rgb: 0x1b1b1b), //!!!
|
||||
unreadBarStrokeColor: UIColor(rgb: 0x000000),
|
||||
unreadBarTextColor: UIColor(rgb: 0xb2b2b2), //!!!
|
||||
dateFillStaticColor: UIColor(rgb: 0x1f1f1f, alpha: 1.0),
|
||||
dateFillFloatingColor: UIColor(rgb: 0xffffff, alpha: 0.2),
|
||||
dateTextColor: UIColor(rgb: 0xb2b2b2)
|
||||
)
|
||||
|
||||
|
||||
@ -275,26 +275,18 @@ private func makeDefaultPresentationTheme(accentColor: UIColor, day: Bool) -> Pr
|
||||
)
|
||||
|
||||
let serviceMessage = PresentationThemeServiceMessage(
|
||||
serviceMessageFillColor: UIColor(rgb: 0x748391, alpha: 0.45),
|
||||
serviceMessagePrimaryTextColor: .white,
|
||||
serviceMessageLinkHighlightColor: UIColor(rgb: 0x748391, alpha: 0.25),
|
||||
components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x748391, alpha: 0.45), primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0x748391, alpha: 0.45), dateFillFloating: UIColor(rgb: 0x939fab, alpha: 0.5)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0x000000, alpha: 0.25), primaryText: .white, linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0x000000, alpha: 0.25), dateFillFloating: UIColor(rgb: 0x000000, alpha: 0.2))),
|
||||
unreadBarFillColor: UIColor(white: 1.0, alpha: 0.9),
|
||||
unreadBarStrokeColor: UIColor(white: 0.0, alpha: 0.2),
|
||||
unreadBarTextColor: UIColor(rgb: 0x86868d),
|
||||
dateFillStaticColor: UIColor(rgb: 0x748391, alpha: 0.45),
|
||||
dateFillFloatingColor: UIColor(rgb: 0x939fab, alpha: 0.5),
|
||||
dateTextColor: .white
|
||||
)
|
||||
|
||||
let serviceMessageDay = PresentationThemeServiceMessage(
|
||||
serviceMessageFillColor: UIColor(rgb: 0xffffff, alpha: 0.8),
|
||||
serviceMessagePrimaryTextColor: UIColor(rgb: 0x8D8E93),
|
||||
serviceMessageLinkHighlightColor: UIColor(rgb: 0x748391, alpha: 0.25),
|
||||
components: PresentationThemeServiceMessageColor(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0xffffff, alpha: 0.8), primaryText: UIColor(rgb: 0x8D8E93), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0xffffff, alpha: 0.8), dateFillFloating: UIColor(rgb: 0xffffff, alpha: 0.8)), withCustomWallpaper: PresentationThemeServiceMessageColorComponents(fill: UIColor(rgb: 0xffffff, alpha: 0.8), primaryText: UIColor(rgb: 0x8D8E93), linkHighlight: UIColor(rgb: 0x748391, alpha: 0.25), dateFillStatic: UIColor(rgb: 0xffffff, alpha: 0.8), dateFillFloating: UIColor(rgb: 0xffffff, alpha: 0.8))),
|
||||
unreadBarFillColor: UIColor(rgb: 0xffffff),
|
||||
unreadBarStrokeColor: UIColor(rgb: 0xffffff),
|
||||
unreadBarTextColor: UIColor(rgb: 0x8D8E93),
|
||||
dateFillStaticColor: UIColor(rgb: 0xffffff, alpha: 0.8),
|
||||
dateFillFloatingColor: UIColor(rgb: 0xffffff, alpha: 0.8),
|
||||
dateTextColor: UIColor(rgb: 0x8D8E93)
|
||||
)
|
||||
|
||||
|
||||
@ -21,6 +21,12 @@ private let identifierDelimiterSet: CharacterSet = {
|
||||
set.formUnion(CharacterSet.whitespacesAndNewlines)
|
||||
return set
|
||||
}()
|
||||
private let externalIdentifierDelimiterSet: CharacterSet = {
|
||||
var set = CharacterSet.punctuationCharacters
|
||||
set.formUnion(CharacterSet.whitespacesAndNewlines)
|
||||
set.remove(".")
|
||||
return set
|
||||
}()
|
||||
|
||||
private enum CurrentEntityType {
|
||||
case command
|
||||
@ -51,6 +57,7 @@ public struct EnabledEntityTypes: OptionSet {
|
||||
public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2)
|
||||
public static let url = EnabledEntityTypes(rawValue: 1 << 3)
|
||||
public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4)
|
||||
public static let external = EnabledEntityTypes(rawValue: 1 << 5)
|
||||
|
||||
public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber]
|
||||
}
|
||||
@ -113,6 +120,8 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
detector = dataDetector
|
||||
}
|
||||
|
||||
let delimiterSet = enabledTypes.contains(.external) ? externalIdentifierDelimiterSet : identifierDelimiterSet
|
||||
|
||||
if let detector = detector {
|
||||
detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in
|
||||
if let result = result {
|
||||
@ -144,7 +153,7 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
if let scalar = scalar {
|
||||
if scalar == "/" {
|
||||
notFound = false
|
||||
if previousScalar != nil && !identifierDelimiterSet.contains(previousScalar!) {
|
||||
if previousScalar != nil && !delimiterSet.contains(previousScalar!) {
|
||||
currentEntity = nil
|
||||
} else {
|
||||
if let (type, range) = currentEntity {
|
||||
@ -178,7 +187,7 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
case .command, .mention:
|
||||
if validIdentifierSet.contains(scalar) {
|
||||
currentEntity = (type, range.lowerBound ..< utf16.index(after: index))
|
||||
} else if identifierDelimiterSet.contains(scalar) {
|
||||
} else if delimiterSet.contains(scalar) {
|
||||
if let (type, range) = currentEntity {
|
||||
commitEntity(utf16, type, range, enabledTypes, &entities)
|
||||
}
|
||||
@ -187,7 +196,7 @@ public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityType
|
||||
case .hashtag:
|
||||
if validHashtagSet.contains(scalar) {
|
||||
currentEntity = (type, range.lowerBound ..< utf16.index(after: index))
|
||||
} else if identifierDelimiterSet.contains(scalar) {
|
||||
} else if delimiterSet.contains(scalar) {
|
||||
if let (type, range) = currentEntity {
|
||||
commitEntity(utf16, type, range, enabledTypes, &entities)
|
||||
}
|
||||
|
||||
@ -88,6 +88,9 @@ final class HashtagSearchController: TelegramController {
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = HashtagSearchControllerNode(account: self.account, peer: self.peer, query: self.query, theme: self.presentationData.theme, strings: self.presentationData.strings)
|
||||
if let chatController = self.controllerNode.chatController {
|
||||
chatController.parentController = self
|
||||
}
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
private let segmentedControl: UISegmentedControl
|
||||
let listNode: ListView
|
||||
|
||||
private var chatController: ChatController?
|
||||
var chatController: ChatController?
|
||||
|
||||
private let account: Account
|
||||
private let query: String
|
||||
|
||||
@ -128,7 +128,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
if self.accountAndMedia == nil || !self.accountAndMedia!.1.media.isEqual(to: imageReference.media) {
|
||||
if let largestSize = largestRepresentationForPhoto(imageReference.media) {
|
||||
let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), emptyColor: .black))()
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference), dispatchOnDisplayLink: false)
|
||||
self.zoomableContent = (largestSize.dimensions, self.imageNode)
|
||||
self.fetchDisposable.set(fetchedMediaResource(postbox: self.account.postbox, reference: imageReference.resourceReference(largestSize.resource)).start())
|
||||
|
||||
434
TelegramUI/InstantPageContentNode.swift
Normal file
434
TelegramUI/InstantPageContentNode.swift
Normal file
@ -0,0 +1,434 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
final class InstantPageContentNode : ASDisplayNode {
|
||||
private let account: Account
|
||||
private let strings: PresentationStrings
|
||||
private let theme: InstantPageTheme
|
||||
|
||||
private let openMedia: (InstantPageMedia) -> Void
|
||||
private let openPeer: (PeerId) -> Void
|
||||
private let openUrl: (InstantPageUrlItem) -> Void
|
||||
|
||||
var currentLayoutTiles: [InstantPageTile] = []
|
||||
var currentLayoutItemsWithNodes: [InstantPageItem] = []
|
||||
var distanceThresholdGroupCount: [Int: Int] = [:]
|
||||
|
||||
var visibleTiles: [Int: InstantPageTileNode] = [:]
|
||||
var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
||||
|
||||
var currentWebEmbedHeights: [Int : CGFloat] = [:]
|
||||
var currentExpandedDetails: [Int : Bool]?
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
|
||||
var requestLayoutUpdate: ((Bool) -> Void)?
|
||||
|
||||
var currentLayout: InstantPageLayout
|
||||
let contentSize: CGSize
|
||||
let inOverlayPanel: Bool
|
||||
|
||||
private var previousVisibleBounds: CGRect?
|
||||
|
||||
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) {
|
||||
self.account = account
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
|
||||
self.openMedia = openMedia
|
||||
self.openPeer = openPeer
|
||||
self.openUrl = openUrl
|
||||
|
||||
self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
self.contentSize = contentSize
|
||||
self.inOverlayPanel = inOverlayPanel
|
||||
|
||||
super.init()
|
||||
|
||||
self.updateLayout()
|
||||
}
|
||||
|
||||
private func updateLayout() {
|
||||
for (_, tileNode) in self.visibleTiles {
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
self.visibleTiles.removeAll()
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width)
|
||||
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
var currentLayoutItemsWithViews: [InstantPageItem] = []
|
||||
var distanceThresholdGroupCount: [Int : Int] = [:]
|
||||
|
||||
var expandedDetails: [Int : Bool] = [:]
|
||||
|
||||
var detailsIndex = -1
|
||||
for item in self.currentLayout.items {
|
||||
if item.wantsNode {
|
||||
currentLayoutItemsWithViews.append(item)
|
||||
if let group = item.distanceThresholdGroup() {
|
||||
let count: Int
|
||||
if let currentCount = distanceThresholdGroupCount[Int(group)] {
|
||||
count = currentCount
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
distanceThresholdGroupCount[Int(group)] = count + 1
|
||||
}
|
||||
if let detailsItem = item as? InstantPageDetailsItem {
|
||||
detailsIndex += 1
|
||||
expandedDetails[detailsIndex] = detailsItem.initiallyExpanded
|
||||
currentDetailsItems.append(detailsItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentExpandedDetails == nil {
|
||||
self.currentExpandedDetails = expandedDetails
|
||||
}
|
||||
|
||||
self.currentLayoutTiles = currentLayoutTiles
|
||||
self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews
|
||||
self.currentDetailsItems = currentDetailsItems
|
||||
self.distanceThresholdGroupCount = distanceThresholdGroupCount
|
||||
}
|
||||
|
||||
var effectiveContentSize: CGSize {
|
||||
var contentSize = self.contentSize
|
||||
for item in self.currentDetailsItems {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
contentSize.height += -item.frame.height + (expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight)
|
||||
}
|
||||
return contentSize
|
||||
}
|
||||
|
||||
func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
|
||||
var visibleTileIndices = Set<Int>()
|
||||
var visibleItemIndices = Set<Int>()
|
||||
|
||||
self.previousVisibleBounds = visibleBounds
|
||||
|
||||
var topNode: ASDisplayNode?
|
||||
let topTileNode = topNode
|
||||
if let scrollSubnodes = self.subnodes {
|
||||
for node in scrollSubnodes.reversed() {
|
||||
if let node = node as? InstantPageTileNode {
|
||||
topNode = node
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var collapseOffset: CGFloat = 0.0
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
var itemIndex = -1
|
||||
var embedIndex = -1
|
||||
var detailsIndex = -1
|
||||
|
||||
for item in self.currentLayoutItemsWithNodes {
|
||||
itemIndex += 1
|
||||
if item is InstantPageWebEmbedItem {
|
||||
embedIndex += 1
|
||||
}
|
||||
if item is InstantPageDetailsItem {
|
||||
detailsIndex += 1
|
||||
}
|
||||
|
||||
var itemThreshold: CGFloat = 0.0
|
||||
if let group = item.distanceThresholdGroup() {
|
||||
var count: Int = 0
|
||||
if let currentCount = self.distanceThresholdGroupCount[group] {
|
||||
count = currentCount
|
||||
}
|
||||
itemThreshold = item.distanceThresholdWithGroupCount(count)
|
||||
}
|
||||
|
||||
var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset)
|
||||
var thresholdedItemFrame = itemFrame
|
||||
thresholdedItemFrame.origin.y -= itemThreshold
|
||||
thresholdedItemFrame.size.height += itemThreshold * 2.0
|
||||
|
||||
if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] {
|
||||
let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight
|
||||
collapseOffset += itemFrame.height - height
|
||||
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height))
|
||||
}
|
||||
|
||||
if visibleBounds.intersects(thresholdedItemFrame) {
|
||||
visibleItemIndices.insert(itemIndex)
|
||||
|
||||
var itemNode = self.visibleItemsWithNodes[itemIndex]
|
||||
if let currentItemNode = itemNode {
|
||||
if !item.matchesNode(currentItemNode) {
|
||||
(currentItemNode as! ASDisplayNode).removeFromSupernode()
|
||||
self.visibleItemsWithNodes.removeValue(forKey: itemIndex)
|
||||
itemNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
if itemNode == nil {
|
||||
let itemIndex = itemIndex
|
||||
let detailsIndex = detailsIndex
|
||||
if let newNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in
|
||||
self?.openMedia(media)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
self?.openPeer(peerId)
|
||||
}, openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
}, updateWebEmbedHeight: { [weak self] height in
|
||||
}, updateDetailsExpanded: { [weak self] expanded in
|
||||
self?.updateDetailsExpanded(detailsIndex, expanded)
|
||||
}, currentExpandedDetails: self.currentExpandedDetails) {
|
||||
newNode.frame = itemFrame
|
||||
newNode.updateLayout(size: itemFrame.size, transition: transition)
|
||||
if let topNode = topNode {
|
||||
self.insertSubnode(newNode, aboveSubnode: topNode)
|
||||
} else {
|
||||
self.insertSubnode(newNode, at: 0)
|
||||
}
|
||||
topNode = newNode
|
||||
self.visibleItemsWithNodes[itemIndex] = newNode
|
||||
itemNode = newNode
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.requestLayoutUpdate = { [weak self] animated in
|
||||
self?.requestLayoutUpdate?(animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (itemNode as! ASDisplayNode).frame != itemFrame {
|
||||
transition.updateFrame(node: (itemNode as! ASDisplayNode), frame: itemFrame)
|
||||
itemNode?.updateLayout(size: itemFrame.size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topNode = topTileNode
|
||||
|
||||
var tileIndex = -1
|
||||
for tile in self.currentLayoutTiles {
|
||||
tileIndex += 1
|
||||
|
||||
let tileFrame = effectiveFrameForTile(tile)
|
||||
var tileVisibleFrame = tileFrame
|
||||
tileVisibleFrame.origin.y -= 400.0
|
||||
tileVisibleFrame.size.height += 400.0 * 2.0
|
||||
if tileVisibleFrame.intersects(visibleBounds) {
|
||||
visibleTileIndices.insert(tileIndex)
|
||||
|
||||
if self.visibleTiles[tileIndex] == nil {
|
||||
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: self.inOverlayPanel ? self.theme.overlayPanelColor : self.theme.pageBackgroundColor)
|
||||
tileNode.frame = tileFrame
|
||||
if let topNode = topNode {
|
||||
self.insertSubnode(tileNode, aboveSubnode: topNode)
|
||||
} else {
|
||||
self.insertSubnode(tileNode, at: 0)
|
||||
}
|
||||
topNode = tileNode
|
||||
self.visibleTiles[tileIndex] = tileNode
|
||||
} else {
|
||||
if visibleTiles[tileIndex]!.frame != tileFrame {
|
||||
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeTileIndices: [Int] = []
|
||||
for (index, tileNode) in self.visibleTiles {
|
||||
if !visibleTileIndices.contains(index) {
|
||||
removeTileIndices.append(index)
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
for index in removeTileIndices {
|
||||
self.visibleTiles.removeValue(forKey: index)
|
||||
}
|
||||
|
||||
var removeItemIndices: [Int] = []
|
||||
for (index, itemNode) in self.visibleItemsWithNodes {
|
||||
if !visibleItemIndices.contains(index) {
|
||||
removeItemIndices.append(index)
|
||||
(itemNode as! ASDisplayNode).removeFromSupernode()
|
||||
} else {
|
||||
var itemFrame = (itemNode as! ASDisplayNode).frame
|
||||
let itemThreshold: CGFloat = 200.0
|
||||
itemFrame.origin.y -= itemThreshold
|
||||
itemFrame.size.height += itemThreshold * 2.0
|
||||
itemNode.updateIsVisible(visibleBounds.intersects(itemFrame))
|
||||
}
|
||||
}
|
||||
for index in removeItemIndices {
|
||||
self.visibleItemsWithNodes.removeValue(forKey: index)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
|
||||
// let currentHeight = self.currentWebEmbedHeights[index]
|
||||
// if height != currentHeight {
|
||||
// if let currentHeight = currentHeight, currentHeight > height {
|
||||
// return
|
||||
// }
|
||||
// self.currentWebEmbedHeights[index] = height
|
||||
//
|
||||
// let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
|
||||
// self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.updateLayout()
|
||||
// strongSelf.updateVisibleItems()
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
|
||||
func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) {
|
||||
if var currentExpandedDetails = self.currentExpandedDetails {
|
||||
currentExpandedDetails[index] = expanded
|
||||
self.currentExpandedDetails = currentExpandedDetails
|
||||
}
|
||||
self.requestLayoutUpdate?(animated)
|
||||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let transitionNode = itemNode.transitionNode(media: media) {
|
||||
return transitionNode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
itemNode.updateHiddenMedia(media: media)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
|
||||
var contentOffset = CGPoint()
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item {
|
||||
contentOffset = itemNode.contentOffset
|
||||
break
|
||||
}
|
||||
}
|
||||
return contentOffset
|
||||
}
|
||||
|
||||
func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
|
||||
return detailsNode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
|
||||
if let node = nodeForDetailsItem(item) {
|
||||
return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight)
|
||||
} else {
|
||||
return item.frame.size
|
||||
}
|
||||
}
|
||||
|
||||
private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect {
|
||||
let layoutOrigin = tile.frame.origin
|
||||
var origin = layoutOrigin
|
||||
for item in self.currentDetailsItems {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
if layoutOrigin.y >= item.frame.maxY {
|
||||
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
||||
origin.y += height - item.frame.height
|
||||
}
|
||||
}
|
||||
return CGRect(origin: origin, size: tile.frame.size)
|
||||
}
|
||||
|
||||
func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
|
||||
let layoutOrigin = item.frame.origin
|
||||
var origin = layoutOrigin
|
||||
|
||||
for item in self.currentDetailsItems {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
if layoutOrigin.y >= item.frame.maxY {
|
||||
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
||||
origin.y += height - item.frame.height
|
||||
}
|
||||
}
|
||||
|
||||
if let item = item as? InstantPageDetailsItem {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
||||
return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height))
|
||||
} else {
|
||||
return CGRect(origin: origin, size: item.frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
|
||||
for item in self.currentLayout.items {
|
||||
let itemFrame = self.effectiveFrameForItem(item)
|
||||
if itemFrame.contains(location) {
|
||||
if let item = item as? InstantPageTextItem, item.selectable {
|
||||
return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY))
|
||||
} else if let item = item as? InstantPageScrollableItem {
|
||||
let contentOffset = scrollableContentOffset(item: item)
|
||||
if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
|
||||
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y))
|
||||
}
|
||||
} else if let item = item as? InstantPageDetailsItem {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
|
||||
if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) {
|
||||
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
|
||||
for item in self.currentLayout.items {
|
||||
let frame = self.effectiveFrameForItem(item)
|
||||
if frame.contains(point) {
|
||||
if item is InstantPagePeerReferenceItem {
|
||||
return .fail
|
||||
} else if item is InstantPageAudioItem {
|
||||
return .fail
|
||||
} else if item is InstantPageArticleItem {
|
||||
return .fail
|
||||
} else if item is InstantPageFeedbackItem {
|
||||
return .fail
|
||||
} else if let item = item as? InstantPageDetailsItem {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
|
||||
return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY))
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
@ -71,6 +71,10 @@ final class InstantPageController: ViewController {
|
||||
self.settingsDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
let _ = updateInstantPageStoredStateInteractively(postbox: self.account.postbox, webPage: self.webPage, state: self.controllerNode.currentState).start()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = InstantPageControllerNode(account: self.account, settings: self.settings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, statusBar: self.statusBar, getNavigationController: { [weak self] in
|
||||
return self?.navigationController as? NavigationController
|
||||
@ -96,7 +100,12 @@ final class InstantPageController: ViewController {
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.controllerNode.updateWebPage(self.webPage, anchor: self.anchor)
|
||||
let _ = (instantPageStoredState(postbox: self.account.postbox, webPage: self.webPage)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updateWebPage(strongSelf.webPage, anchor: strongSelf.anchor, state: state)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
@ -22,6 +22,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var webPage: TelegramMediaWebpage?
|
||||
private var initialAnchor: String?
|
||||
private var pendingAnchor: String?
|
||||
private var initialState: InstantPageStoredState?
|
||||
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
private var setupScrollOffsetOnLayout: Bool = false
|
||||
@ -62,6 +63,16 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
private var themeReferenceDate: Date?
|
||||
|
||||
var currentState: InstantPageStoredState {
|
||||
var details: [InstantPageStoredDetailsState] = []
|
||||
if let currentExpandedDetails = self.currentExpandedDetails {
|
||||
for (index, expanded) in currentExpandedDetails {
|
||||
details.append(InstantPageStoredDetailsState(index: Int32(clamping: index), expanded: expanded, details: []))
|
||||
}
|
||||
}
|
||||
return InstantPageStoredState(contentOffset: Double(self.scrollNode.view.contentOffset.y), details: details)
|
||||
}
|
||||
|
||||
init(account: Account, settings: InstantPagePresentationSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, statusBar: StatusBar, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.presentationTheme = presentationTheme
|
||||
@ -246,7 +257,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.scrollNode.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?) {
|
||||
func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) {
|
||||
if self.webPage != webPage {
|
||||
if self.webPage != nil && self.currentLayout != nil {
|
||||
if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
@ -259,8 +270,18 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
self.setupScrollOffsetOnLayout = self.webPage == nil
|
||||
self.webPage = webPage
|
||||
self.initialAnchor = anchor?.removingPercentEncoding
|
||||
|
||||
if let anchor = anchor {
|
||||
self.initialAnchor = anchor.removingPercentEncoding
|
||||
} else if let state = state {
|
||||
self.initialState = state
|
||||
if !state.details.isEmpty {
|
||||
var storedExpandedDetails: [Int: Bool] = [:]
|
||||
for state in state.details {
|
||||
storedExpandedDetails[Int(clamping: state.index)] = state.expanded
|
||||
}
|
||||
self.currentExpandedDetails = storedExpandedDetails
|
||||
}
|
||||
}
|
||||
self.currentLayout = nil
|
||||
self.updateLayout()
|
||||
|
||||
@ -317,14 +338,15 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
if resetOffset {
|
||||
var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top)
|
||||
if let anchor = self.initialAnchor, !anchor.isEmpty {
|
||||
if let state = self.initialState {
|
||||
self.setupScrollOffsetOnLayout = false
|
||||
contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset))
|
||||
}
|
||||
else if let anchor = self.initialAnchor, !anchor.isEmpty {
|
||||
if let items = self.currentLayout?.items {
|
||||
self.setupScrollOffsetOnLayout = false
|
||||
outer: for item in items {
|
||||
if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
|
||||
contentOffset = CGPoint(x: 0.0, y: item.frame.origin.y - self.scrollNode.view.contentInset.top)
|
||||
break outer
|
||||
}
|
||||
if let (item, lineOffset, detailsItems) = self.findAnchorItem(anchor, items: items) {
|
||||
contentOffset = CGPoint(x: 0.0, y: item.frame.minY - self.scrollNode.view.contentInset.top)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -702,11 +724,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var rects: [CGRect]?
|
||||
if let location = location, let currentLayout = self.currentLayout {
|
||||
for item in currentLayout.items {
|
||||
let itemFrame = effectiveFrameForItem(item)
|
||||
let itemFrame = self.effectiveFrameForItem(item)
|
||||
if itemFrame.contains(location) {
|
||||
var contentOffset = CGPoint()
|
||||
if let item = item as? InstantPageScrollableItem {
|
||||
contentOffset = scrollableContentOffset(item: item)
|
||||
contentOffset = self.scrollableContentOffset(item: item)
|
||||
}
|
||||
var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY))
|
||||
|
||||
@ -848,10 +870,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.openUrl(url)
|
||||
}
|
||||
case .longTap:
|
||||
if let url = self.urlForTapLocation(location) {
|
||||
if let theme = self.theme, let url = self.urlForTapLocation(location) {
|
||||
let canOpenIn = availableOpenInOptions(applicationContext: self.account.telegramApplicationContext, item: .url(url: url.url)).count > 1
|
||||
let openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen
|
||||
let actionSheet = ActionSheetController(presentationTheme: self.presentationTheme)
|
||||
let actionSheet = ActionSheetController(instantPageTheme: theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: url.url),
|
||||
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
@ -968,7 +990,16 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func presentReferenceView(item: InstantPageTextItem) {
|
||||
let controller = InstantPageReferenceController(account: self.account, item: item)
|
||||
guard let theme = self.theme, let webPage = self.webPage else {
|
||||
return
|
||||
}
|
||||
let controller = InstantPageReferenceController(account: self.account, theme: theme, webPage: webPage, item: item, openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
}, openUrlIn: { [weak self] url in
|
||||
self?.openUrlIn(url)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.present(c, a)
|
||||
})
|
||||
self.present(controller, nil)
|
||||
}
|
||||
|
||||
@ -979,40 +1010,44 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
if !anchor.isEmpty {
|
||||
if let (item, lineOffset, detailsItems) = findAnchorItem(String(anchor), items: items) {
|
||||
var previousDetailsNode: InstantPageDetailsNode?
|
||||
var containerOffset: CGFloat = 0.0
|
||||
for detailsItem in detailsItems {
|
||||
if let previousNode = previousDetailsNode {
|
||||
previousNode.contentNode.updateDetailsExpanded(detailsItem.index, true, animated: false)
|
||||
let frame = previousNode.effectiveFrameForItem(detailsItem)
|
||||
containerOffset += frame.minY
|
||||
|
||||
previousDetailsNode = previousNode.contentNode.nodeForDetailsItem(detailsItem)
|
||||
previousDetailsNode?.setExpanded(true, animated: false)
|
||||
} else {
|
||||
self.updateDetailsExpanded(detailsItem.index, true, animated: false)
|
||||
let frame = self.effectiveFrameForItem(detailsItem)
|
||||
containerOffset += frame.minY
|
||||
|
||||
previousDetailsNode = self.nodeForDetailsItem(detailsItem)
|
||||
previousDetailsNode?.setExpanded(true, animated: false)
|
||||
if let item = item as? InstantPageTextItem, item.plainText().count > 1 {
|
||||
self.presentReferenceView(item: item)
|
||||
} else {
|
||||
var previousDetailsNode: InstantPageDetailsNode?
|
||||
var containerOffset: CGFloat = 0.0
|
||||
for detailsItem in detailsItems {
|
||||
if let previousNode = previousDetailsNode {
|
||||
previousNode.contentNode.updateDetailsExpanded(detailsItem.index, true, animated: false)
|
||||
let frame = previousNode.effectiveFrameForItem(detailsItem)
|
||||
containerOffset += frame.minY
|
||||
|
||||
previousDetailsNode = previousNode.contentNode.nodeForDetailsItem(detailsItem)
|
||||
previousDetailsNode?.setExpanded(true, animated: false)
|
||||
} else {
|
||||
self.updateDetailsExpanded(detailsItem.index, true, animated: false)
|
||||
let frame = self.effectiveFrameForItem(detailsItem)
|
||||
containerOffset += frame.minY
|
||||
|
||||
previousDetailsNode = self.nodeForDetailsItem(detailsItem)
|
||||
previousDetailsNode?.setExpanded(true, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
let frame: CGRect
|
||||
if let previousDetailsNode = previousDetailsNode {
|
||||
frame = previousDetailsNode.effectiveFrameForItem(item)
|
||||
} else {
|
||||
frame = self.effectiveFrameForItem(item)
|
||||
}
|
||||
|
||||
var targetY = min(containerOffset + frame.minY + lineOffset, self.scrollNode.view.contentSize.height - self.scrollNode.frame.height)
|
||||
if targetY < self.scrollNode.view.contentOffset.y {
|
||||
targetY -= self.scrollNode.view.contentInset.top
|
||||
} else {
|
||||
targetY -= self.containerLayout?.statusBarHeight ?? 20.0
|
||||
}
|
||||
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true)
|
||||
}
|
||||
|
||||
let frame: CGRect
|
||||
if let previousDetailsNode = previousDetailsNode {
|
||||
frame = previousDetailsNode.effectiveFrameForItem(item)
|
||||
} else {
|
||||
frame = self.effectiveFrameForItem(item)
|
||||
}
|
||||
|
||||
var targetY = min(containerOffset + frame.minY + lineOffset, self.scrollNode.view.contentSize.height - self.scrollNode.frame.height)
|
||||
if targetY < self.scrollNode.view.contentOffset.y {
|
||||
targetY -= self.scrollNode.view.contentInset.top
|
||||
} else {
|
||||
targetY -= self.containerLayout?.statusBarHeight ?? 20.0
|
||||
}
|
||||
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true)
|
||||
} else if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, !instantPage.isComplete {
|
||||
self.loadProgress.set(0.5)
|
||||
self.pendingAnchor = anchor
|
||||
@ -1037,14 +1072,11 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
self.loadProgress.set(0.0)
|
||||
self.loadProgress.set(0.02)
|
||||
let resolveSignal = resolveUrl(account: self.account, url: url.url)
|
||||
|> afterCompleted { [weak self] in
|
||||
self?.loadProgress.set(0.07)
|
||||
}
|
||||
|
||||
|
||||
self.loadWebpageDisposable.set(nil)
|
||||
self.resolveUrlDisposable.set((resolveSignal |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
self.resolveUrlDisposable.set((resolveUrl(account: self.account, url: url.url) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
strongSelf.loadProgress.set(0.07)
|
||||
switch result {
|
||||
case let .externalUrl(externalUrl):
|
||||
if let webpageId = url.webpageId {
|
||||
@ -1068,11 +1100,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
strongSelf.loadProgress.set(1.0)
|
||||
openExternalUrl(account: strongSelf.account, url: externalUrl, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, applicationContext: strongSelf.account.telegramApplicationContext, navigationController: strongSelf.getNavigationController(), dismissInput: {
|
||||
self?.view.endEditing(true)
|
||||
})
|
||||
}
|
||||
default:
|
||||
strongSelf.loadProgress.set(1.0)
|
||||
openResolvedUrl(result, account: strongSelf.account, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, navigation in
|
||||
switch navigation {
|
||||
case let .chat(_, messageId):
|
||||
|
||||
@ -8,432 +8,6 @@ import SwiftSignalKit
|
||||
private let detailsInset: CGFloat = 17.0
|
||||
private let titleInset: CGFloat = 22.0
|
||||
|
||||
final class InstantPageDetailsContentNode : ASDisplayNode {
|
||||
private let account: Account
|
||||
private let strings: PresentationStrings
|
||||
private let theme: InstantPageTheme
|
||||
|
||||
private let openMedia: (InstantPageMedia) -> Void
|
||||
private let openPeer: (PeerId) -> Void
|
||||
private let openUrl: (InstantPageUrlItem) -> Void
|
||||
|
||||
var currentLayoutTiles: [InstantPageTile] = []
|
||||
var currentLayoutItemsWithNodes: [InstantPageItem] = []
|
||||
var distanceThresholdGroupCount: [Int: Int] = [:]
|
||||
|
||||
var visibleTiles: [Int: InstantPageTileNode] = [:]
|
||||
var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
||||
|
||||
var currentWebEmbedHeights: [Int : CGFloat] = [:]
|
||||
var currentExpandedDetails: [Int : Bool]?
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
|
||||
var requestLayoutUpdate: ((Bool) -> Void)?
|
||||
|
||||
var currentLayout: InstantPageLayout
|
||||
let contentSize: CGSize
|
||||
|
||||
private var previousVisibleBounds: CGRect?
|
||||
|
||||
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) {
|
||||
self.account = account
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
|
||||
self.openMedia = openMedia
|
||||
self.openPeer = openPeer
|
||||
self.openUrl = openUrl
|
||||
|
||||
self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
self.contentSize = contentSize
|
||||
|
||||
super.init()
|
||||
|
||||
self.updateLayout()
|
||||
}
|
||||
|
||||
private func updateLayout() {
|
||||
for (_, tileNode) in self.visibleTiles {
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
self.visibleTiles.removeAll()
|
||||
|
||||
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width)
|
||||
|
||||
var currentDetailsItems: [InstantPageDetailsItem] = []
|
||||
var currentLayoutItemsWithViews: [InstantPageItem] = []
|
||||
var distanceThresholdGroupCount: [Int : Int] = [:]
|
||||
|
||||
var expandedDetails: [Int : Bool] = [:]
|
||||
|
||||
var detailsIndex = -1
|
||||
for item in self.currentLayout.items {
|
||||
if item.wantsNode {
|
||||
currentLayoutItemsWithViews.append(item)
|
||||
if let group = item.distanceThresholdGroup() {
|
||||
let count: Int
|
||||
if let currentCount = distanceThresholdGroupCount[Int(group)] {
|
||||
count = currentCount
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
distanceThresholdGroupCount[Int(group)] = count + 1
|
||||
}
|
||||
if let detailsItem = item as? InstantPageDetailsItem {
|
||||
detailsIndex += 1
|
||||
expandedDetails[detailsIndex] = detailsItem.initiallyExpanded
|
||||
currentDetailsItems.append(detailsItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentExpandedDetails == nil {
|
||||
self.currentExpandedDetails = expandedDetails
|
||||
}
|
||||
|
||||
self.currentLayoutTiles = currentLayoutTiles
|
||||
self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews
|
||||
self.currentDetailsItems = currentDetailsItems
|
||||
self.distanceThresholdGroupCount = distanceThresholdGroupCount
|
||||
}
|
||||
|
||||
var effectiveContentSize: CGSize {
|
||||
var contentSize = self.contentSize
|
||||
for item in self.currentDetailsItems {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
contentSize.height += -item.frame.height + (expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight)
|
||||
}
|
||||
return contentSize
|
||||
}
|
||||
|
||||
func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
|
||||
var visibleTileIndices = Set<Int>()
|
||||
var visibleItemIndices = Set<Int>()
|
||||
|
||||
self.previousVisibleBounds = visibleBounds
|
||||
|
||||
var topNode: ASDisplayNode?
|
||||
let topTileNode = topNode
|
||||
if let scrollSubnodes = self.subnodes {
|
||||
for node in scrollSubnodes.reversed() {
|
||||
if let node = node as? InstantPageTileNode {
|
||||
topNode = node
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var collapseOffset: CGFloat = 0.0
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
var itemIndex = -1
|
||||
var embedIndex = -1
|
||||
var detailsIndex = -1
|
||||
|
||||
for item in self.currentLayoutItemsWithNodes {
|
||||
itemIndex += 1
|
||||
if item is InstantPageWebEmbedItem {
|
||||
embedIndex += 1
|
||||
}
|
||||
if item is InstantPageDetailsItem {
|
||||
detailsIndex += 1
|
||||
}
|
||||
|
||||
var itemThreshold: CGFloat = 0.0
|
||||
if let group = item.distanceThresholdGroup() {
|
||||
var count: Int = 0
|
||||
if let currentCount = self.distanceThresholdGroupCount[group] {
|
||||
count = currentCount
|
||||
}
|
||||
itemThreshold = item.distanceThresholdWithGroupCount(count)
|
||||
}
|
||||
|
||||
var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset)
|
||||
var thresholdedItemFrame = itemFrame
|
||||
thresholdedItemFrame.origin.y -= itemThreshold
|
||||
thresholdedItemFrame.size.height += itemThreshold * 2.0
|
||||
|
||||
if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] {
|
||||
let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight
|
||||
collapseOffset += itemFrame.height - height
|
||||
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height))
|
||||
}
|
||||
|
||||
if visibleBounds.intersects(thresholdedItemFrame) {
|
||||
visibleItemIndices.insert(itemIndex)
|
||||
|
||||
var itemNode = self.visibleItemsWithNodes[itemIndex]
|
||||
if let currentItemNode = itemNode {
|
||||
if !item.matchesNode(currentItemNode) {
|
||||
(currentItemNode as! ASDisplayNode).removeFromSupernode()
|
||||
self.visibleItemsWithNodes.removeValue(forKey: itemIndex)
|
||||
itemNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
if itemNode == nil {
|
||||
let itemIndex = itemIndex
|
||||
let detailsIndex = detailsIndex
|
||||
if let newNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in
|
||||
self?.openMedia(media)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
self?.openPeer(peerId)
|
||||
}, openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
}, updateWebEmbedHeight: { [weak self] height in
|
||||
}, updateDetailsExpanded: { [weak self] expanded in
|
||||
self?.updateDetailsExpanded(detailsIndex, expanded)
|
||||
}, currentExpandedDetails: self.currentExpandedDetails) {
|
||||
newNode.frame = itemFrame
|
||||
newNode.updateLayout(size: itemFrame.size, transition: transition)
|
||||
if let topNode = topNode {
|
||||
self.insertSubnode(newNode, aboveSubnode: topNode)
|
||||
} else {
|
||||
self.insertSubnode(newNode, at: 0)
|
||||
}
|
||||
topNode = newNode
|
||||
self.visibleItemsWithNodes[itemIndex] = newNode
|
||||
itemNode = newNode
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.requestLayoutUpdate = { [weak self] animated in
|
||||
self?.requestLayoutUpdate?(animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (itemNode as! ASDisplayNode).frame != itemFrame {
|
||||
transition.updateFrame(node: (itemNode as! ASDisplayNode), frame: itemFrame)
|
||||
itemNode?.updateLayout(size: itemFrame.size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode {
|
||||
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topNode = topTileNode
|
||||
|
||||
var tileIndex = -1
|
||||
for tile in self.currentLayoutTiles {
|
||||
tileIndex += 1
|
||||
|
||||
let tileFrame = effectiveFrameForTile(tile)
|
||||
var tileVisibleFrame = tileFrame
|
||||
tileVisibleFrame.origin.y -= 400.0
|
||||
tileVisibleFrame.size.height += 400.0 * 2.0
|
||||
if tileVisibleFrame.intersects(visibleBounds) {
|
||||
visibleTileIndices.insert(tileIndex)
|
||||
|
||||
if self.visibleTiles[tileIndex] == nil {
|
||||
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor)
|
||||
tileNode.frame = tileFrame
|
||||
if let topNode = topNode {
|
||||
self.insertSubnode(tileNode, aboveSubnode: topNode)
|
||||
} else {
|
||||
self.insertSubnode(tileNode, at: 0)
|
||||
}
|
||||
topNode = tileNode
|
||||
self.visibleTiles[tileIndex] = tileNode
|
||||
} else {
|
||||
if visibleTiles[tileIndex]!.frame != tileFrame {
|
||||
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeTileIndices: [Int] = []
|
||||
for (index, tileNode) in self.visibleTiles {
|
||||
if !visibleTileIndices.contains(index) {
|
||||
removeTileIndices.append(index)
|
||||
tileNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
for index in removeTileIndices {
|
||||
self.visibleTiles.removeValue(forKey: index)
|
||||
}
|
||||
|
||||
var removeItemIndices: [Int] = []
|
||||
for (index, itemNode) in self.visibleItemsWithNodes {
|
||||
if !visibleItemIndices.contains(index) {
|
||||
removeItemIndices.append(index)
|
||||
(itemNode as! ASDisplayNode).removeFromSupernode()
|
||||
} else {
|
||||
var itemFrame = (itemNode as! ASDisplayNode).frame
|
||||
let itemThreshold: CGFloat = 200.0
|
||||
itemFrame.origin.y -= itemThreshold
|
||||
itemFrame.size.height += itemThreshold * 2.0
|
||||
itemNode.updateIsVisible(visibleBounds.intersects(itemFrame))
|
||||
}
|
||||
}
|
||||
for index in removeItemIndices {
|
||||
self.visibleItemsWithNodes.removeValue(forKey: index)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
|
||||
// let currentHeight = self.currentWebEmbedHeights[index]
|
||||
// if height != currentHeight {
|
||||
// if let currentHeight = currentHeight, currentHeight > height {
|
||||
// return
|
||||
// }
|
||||
// self.currentWebEmbedHeights[index] = height
|
||||
//
|
||||
// let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
|
||||
// self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.updateLayout()
|
||||
// strongSelf.updateVisibleItems()
|
||||
// }
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
|
||||
func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) {
|
||||
if var currentExpandedDetails = self.currentExpandedDetails {
|
||||
currentExpandedDetails[index] = expanded
|
||||
self.currentExpandedDetails = currentExpandedDetails
|
||||
}
|
||||
self.requestLayoutUpdate?(animated)
|
||||
}
|
||||
|
||||
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let transitionNode = itemNode.transitionNode(media: media) {
|
||||
return transitionNode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateHiddenMedia(media: InstantPageMedia?) {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
itemNode.updateHiddenMedia(media: media)
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
|
||||
var contentOffset = CGPoint()
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item {
|
||||
contentOffset = itemNode.contentOffset
|
||||
break
|
||||
}
|
||||
}
|
||||
return contentOffset
|
||||
}
|
||||
|
||||
func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
|
||||
return detailsNode
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
|
||||
if let node = nodeForDetailsItem(item) {
|
||||
return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight)
|
||||
} else {
|
||||
return item.frame.size
|
||||
}
|
||||
}
|
||||
|
||||
private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect {
|
||||
let layoutOrigin = tile.frame.origin
|
||||
var origin = layoutOrigin
|
||||
for item in self.currentDetailsItems {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
if layoutOrigin.y >= item.frame.maxY {
|
||||
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
||||
origin.y += height - item.frame.height
|
||||
}
|
||||
}
|
||||
return CGRect(origin: origin, size: tile.frame.size)
|
||||
}
|
||||
|
||||
fileprivate func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
|
||||
let layoutOrigin = item.frame.origin
|
||||
var origin = layoutOrigin
|
||||
|
||||
for item in self.currentDetailsItems {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
if layoutOrigin.y >= item.frame.maxY {
|
||||
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
||||
origin.y += height - item.frame.height
|
||||
}
|
||||
}
|
||||
|
||||
if let item = item as? InstantPageDetailsItem {
|
||||
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
||||
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
||||
return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height))
|
||||
} else {
|
||||
return CGRect(origin: origin, size: item.frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
|
||||
for item in self.currentLayout.items {
|
||||
let itemFrame = self.effectiveFrameForItem(item)
|
||||
if itemFrame.contains(location) {
|
||||
if let item = item as? InstantPageTextItem, item.selectable {
|
||||
return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY))
|
||||
} else if let item = item as? InstantPageScrollableItem {
|
||||
let contentOffset = scrollableContentOffset(item: item)
|
||||
if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
|
||||
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y))
|
||||
}
|
||||
} else if let item = item as? InstantPageDetailsItem {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
|
||||
if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) {
|
||||
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
|
||||
for item in self.currentLayout.items {
|
||||
let frame = self.effectiveFrameForItem(item)
|
||||
if frame.contains(point) {
|
||||
if item is InstantPagePeerReferenceItem {
|
||||
return .fail
|
||||
} else if item is InstantPageAudioItem {
|
||||
return .fail
|
||||
} else if item is InstantPageArticleItem {
|
||||
return .fail
|
||||
} else if item is InstantPageFeedbackItem {
|
||||
return .fail
|
||||
} else if let item = item as? InstantPageDetailsItem {
|
||||
for (_, itemNode) in self.visibleItemsWithNodes {
|
||||
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
|
||||
return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY))
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return .waitForSingleTap
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
private let strings: PresentationStrings
|
||||
@ -447,7 +21,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let arrowNode: InstantPageDetailsArrowNode
|
||||
let separatorNode: ASDisplayNode
|
||||
let contentNode: InstantPageDetailsContentNode
|
||||
let contentNode: InstantPageContentNode
|
||||
|
||||
private let updateExpanded: (Bool) -> Void
|
||||
var expanded: Bool
|
||||
@ -485,7 +59,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
|
||||
self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded)
|
||||
self.separatorNode = ASDisplayNode()
|
||||
|
||||
self.contentNode = InstantPageDetailsContentNode(account: account, strings: strings, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, openPeer: openPeer, openUrl: openUrl)
|
||||
self.contentNode = InstantPageContentNode(account: account, strings: strings, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, openPeer: openPeer, openUrl: openUrl)
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
@ -23,7 +23,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var fetchStatus: MediaResourceStatus?
|
||||
private var fetchedDisposable = MetaDisposable()
|
||||
private var statusDisposable = MetaDisposable()
|
||||
|
||||
private var themeUpdated: Bool = false
|
||||
|
||||
@ -47,15 +49,26 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
if let image = media.media as? TelegramMediaImage {
|
||||
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference))
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
|
||||
|
||||
self.statusDisposable.set((account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
displayLinkDispatcher.dispatch {
|
||||
if let strongSelf = self {
|
||||
strongSelf.fetchStatus = status
|
||||
strongSelf.updateFetchStatus()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if media.url != nil {
|
||||
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
|
||||
self.addSubnode(self.linkIconNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.statusNode)
|
||||
} else if let file = media.media as? TelegramMediaFile {
|
||||
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
||||
if file.mimeType.hasPrefix("image/") {
|
||||
@ -68,7 +81,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
self.addSubnode(self.pinNode)
|
||||
|
||||
var zoom: Int32 = 12
|
||||
var dimensions = CGSize(width: 200, height: 100)
|
||||
var dimensions = CGSize(width: 200.0, height: 100.0)
|
||||
for attribute in self.attributes {
|
||||
if let mapAttribute = attribute as? InstantPageMapAttribute {
|
||||
zoom = mapAttribute.zoom
|
||||
@ -83,12 +96,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
self.imageNode.setSignal(chatMessagePhoto(postbox: account.postbox, photoReference: imageReference))
|
||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
|
||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||
self.addSubnode(statusNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchedDisposable.dispose()
|
||||
self.statusDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -115,6 +129,27 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFetchStatus() {
|
||||
var state: RadialStatusNodeState = .none
|
||||
if let fetchStatus = self.fetchStatus {
|
||||
switch fetchStatus {
|
||||
case let .Fetching(isActive, progress):
|
||||
var adjustedProgress = progress
|
||||
if isActive {
|
||||
adjustedProgress = max(adjustedProgress, 0.027)
|
||||
}
|
||||
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
self.statusNode.transitionToState(state, completion: { [weak statusNode] in
|
||||
if state == .none {
|
||||
statusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
@ -126,12 +161,15 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let radialStatusSize: CGFloat = 50.0
|
||||
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
|
||||
|
||||
if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
||||
let imageSize = largest.dimensions.aspectFilled(size)
|
||||
let boundingSize = size
|
||||
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.theme.pageBackgroundColor))
|
||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.theme.panelBackgroundColor))
|
||||
apply()
|
||||
|
||||
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
|
||||
@ -168,9 +206,6 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.theme.pageBackgroundColor))
|
||||
apply()
|
||||
|
||||
let radialStatusSize: CGFloat = 50.0
|
||||
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem {
|
||||
|
||||
func matchesNode(_ node: InstantPageNode) -> Bool {
|
||||
if let node = node as? InstantPagePeerReferenceNode {
|
||||
return self.initialPeer.id == node.initialPeer.id && self.safeInset == node.safeInset
|
||||
return self.initialPeer.id == node.peer?.id && self.safeInset == node.safeInset
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -43,7 +43,6 @@ private enum JoinState: Equatable {
|
||||
|
||||
final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
private let account: Account
|
||||
let initialPeer: Peer
|
||||
let safeInset: CGFloat
|
||||
private let transparent: Bool
|
||||
private let rtl: Bool
|
||||
@ -58,18 +57,17 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
private let activityIndicator: ActivityIndicator
|
||||
private let checkNode: ASImageNode
|
||||
|
||||
private var peer: Peer?
|
||||
var peer: Peer?
|
||||
private var peerDisposable: Disposable?
|
||||
|
||||
private let joinDisposable = MetaDisposable()
|
||||
|
||||
private var joinState: JoinState = .none
|
||||
|
||||
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (PeerId) -> Void) {
|
||||
self.account = account
|
||||
self.strings = strings
|
||||
self.theme = theme
|
||||
self.initialPeer = initialPeer
|
||||
self.peer = initialPeer
|
||||
self.safeInset = safeInset
|
||||
self.transparent = transparent
|
||||
self.rtl = rtl
|
||||
@ -140,7 +138,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
self.joinNode.addTarget(self, action: #selector(self.joinPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
let account = self.account
|
||||
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: self.initialPeer)
|
||||
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer)
|
||||
|> mapToSignal({ peer -> Signal<Peer, NoError> in
|
||||
if let peer = peer as? TelegramChannel, let username = peer.username, peer.accessHash == nil {
|
||||
return .single(peer) |> then(resolvePeerByName(account: account, name: username)
|
||||
@ -160,8 +158,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
if let strongSelf = self {
|
||||
let textColor = strongSelf.transparent ? UIColor.white : strongSelf.theme.panelPrimaryColor
|
||||
strongSelf.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: textColor)
|
||||
strongSelf.peer = peer
|
||||
if let peer = peer as? TelegramChannel {
|
||||
var joinState = strongSelf.joinState
|
||||
if case .member = peer.participationStatus {
|
||||
@ -178,6 +175,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
}
|
||||
strongSelf.updateJoinState(joinState)
|
||||
}
|
||||
strongSelf.applyThemeAndStrings(themeUpdated: false)
|
||||
strongSelf.setNeedsLayout()
|
||||
}
|
||||
})
|
||||
@ -214,6 +212,11 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
let secondaryColor = self.transparent ? UIColor.white : self.theme.panelSecondaryColor
|
||||
self.checkNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/PanelCheck"), color: secondaryColor)
|
||||
self.activityIndicator.type = .custom(self.theme.panelAccentColor, 22.0, 2.0, false)
|
||||
|
||||
if !self.transparent {
|
||||
self.backgroundColor = self.theme.panelBackgroundColor
|
||||
self.highlightedBackgroundNode.backgroundColor = self.theme.panelHighlightedBackgroundColor
|
||||
}
|
||||
}
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
@ -289,13 +292,15 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
self.openPeer(self.initialPeer.id)
|
||||
if let peer = self.peer {
|
||||
self.openPeer(peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func joinPressed() {
|
||||
if case .notJoined = self.joinState {
|
||||
if let peer = self.peer, case .notJoined = self.joinState {
|
||||
self.updateJoinState(.inProgress)
|
||||
self.joinDisposable.set((joinChannel(account: self.account, peerId: self.initialPeer.id) |> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
self.joinDisposable.set((joinChannel(account: self.account, peerId: peer.id) |> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if case .inProgress = strongSelf.joinState {
|
||||
strongSelf.updateJoinState(.notJoined)
|
||||
|
||||
@ -13,11 +13,21 @@ final class InstantPageReferenceController: ViewController {
|
||||
private var animatedIn = false
|
||||
|
||||
private let account: Account
|
||||
private let theme: InstantPageTheme
|
||||
private let webPage: TelegramMediaWebpage
|
||||
private let item: InstantPageTextItem
|
||||
private let openUrl: (InstantPageUrlItem) -> Void
|
||||
private let openUrlIn: (InstantPageUrlItem) -> Void
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
|
||||
init(account: Account, item: InstantPageTextItem) {
|
||||
init(account: Account, theme: InstantPageTheme, webPage: TelegramMediaWebpage, item: InstantPageTextItem, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.webPage = webPage
|
||||
self.item = item
|
||||
self.openUrl = openUrl
|
||||
self.openUrlIn = openUrlIn
|
||||
self.present = present
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -29,7 +39,7 @@ final class InstantPageReferenceController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = InstantPageReferenceControllerNode(account: self.account, item: self.item)
|
||||
self.displayNode = InstantPageReferenceControllerNode(account: self.account, theme: self.theme, webPage: self.webPage, item: self.item, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present)
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
@ -2,28 +2,44 @@ import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SafariServices
|
||||
|
||||
class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||
private let account: Account
|
||||
private let theme: InstantPageTheme
|
||||
private var presentationData: PresentationData
|
||||
private let webPage: TelegramMediaWebpage
|
||||
private let item: InstantPageTextItem
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
private let wrappingScrollNode: ASScrollNode
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
private let contentBackgroundNode: ASImageNode
|
||||
|
||||
private var contentNode: InstantPageContentNode?
|
||||
private let titleNode: ASTextNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
|
||||
private let closeButton: HighlightableButtonNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
private var textSelectionNode: LinkHighlightingNode?
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private var openUrl: (InstantPageUrlItem) -> Void
|
||||
private var openUrlIn: (InstantPageUrlItem) -> Void
|
||||
private var present: (ViewController, Any?) -> Void
|
||||
|
||||
var dismiss: (() -> Void)?
|
||||
var close: (() -> Void)?
|
||||
|
||||
init(account: Account, item: InstantPageTextItem) {
|
||||
init(account: Account, theme: InstantPageTheme, webPage: TelegramMediaWebpage, item: InstantPageTextItem, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.account = account
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
self.theme = theme
|
||||
self.webPage = webPage
|
||||
self.item = item
|
||||
self.openUrl = openUrl
|
||||
self.openUrlIn = openUrlIn
|
||||
self.present = present
|
||||
|
||||
self.wrappingScrollNode = ASScrollNode()
|
||||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
@ -33,7 +49,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
|
||||
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: self.theme.overlayPanelColor)
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.isOpaque = false
|
||||
@ -45,10 +61,10 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
self.contentBackgroundNode.image = roundedBackground
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ShareMenu_ShareTo, font: Font.medium(20.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InstantPage_Reference, font: Font.medium(17.0), textColor: self.theme.panelSecondaryColor)
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
|
||||
self.separatorNode.backgroundColor = self.theme.controlColor
|
||||
|
||||
self.closeButton = HighlightableButtonNode()
|
||||
|
||||
@ -65,6 +81,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.contentBackgroundNode)
|
||||
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
|
||||
|
||||
self.contentContainerNode.addSubnode(self.titleNode)
|
||||
self.contentContainerNode.addSubnode(self.separatorNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -73,6 +92,21 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
recognizer.delaysTouchesBegan = false
|
||||
recognizer.tapActionAtPoint = { [weak self] point in
|
||||
if let strongSelf = self, let contentNode = strongSelf.contentNode {
|
||||
return strongSelf.tapActionAtPoint(point.offsetBy(dx: -contentNode.frame.minX, dy: -contentNode.frame.minY))
|
||||
}
|
||||
return .waitForSingleTap
|
||||
}
|
||||
recognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self, let contentNode = strongSelf.contentNode {
|
||||
strongSelf.updateTouchesAtPoint(point?.offsetBy(dx: -contentNode.frame.minX, dy: -contentNode.frame.minY))
|
||||
}
|
||||
}
|
||||
self.contentContainerNode.view.addGestureRecognizer(recognizer)
|
||||
}
|
||||
|
||||
@objc func closeButtonPressed() {
|
||||
@ -139,37 +173,47 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
||||
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
if insets.bottom > 0 {
|
||||
bottomInset -= 12.0
|
||||
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
let titleAreaHeight: CGFloat = 54.0
|
||||
var contentHeight = titleAreaHeight + bottomInset
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
if self.contentNode == nil || self.contentNode?.frame.width != width {
|
||||
self.contentNode?.removeFromSupernode()
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let (_, items, contentSize) = layoutTextItemWithString(self.item.attributedString, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: 0.0, y: sideInset))
|
||||
let contentNode = InstantPageContentNode(account: self.account, strings: self.presentationData.strings, theme: self.theme, items: items, contentSize: contentSize, inOverlayPanel: true, openMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in })
|
||||
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: sideInset, y: titleAreaHeight), size: contentSize))
|
||||
self.contentContainerNode.insertSubnode(contentNode, at: 0)
|
||||
self.contentNode = contentNode
|
||||
|
||||
contentHeight += contentSize.height + sideInset
|
||||
|
||||
contentNode.updateVisibleItems(visibleBounds: contentNode.bounds, animated: false)
|
||||
}
|
||||
|
||||
let titleAreaHeight: CGFloat = 54.0
|
||||
|
||||
let buttonHeight: CGFloat = 57.0
|
||||
let sectionSpacing: CGFloat = 8.0
|
||||
|
||||
let maximumContentHeight = layout.size.height - insets.top - max(bottomInset + buttonHeight, insets.bottom) - sectionSpacing
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left)
|
||||
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
|
||||
let contentFrame = contentContainerFrame
|
||||
|
||||
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
|
||||
let contentFrame = contentContainerFrame.insetBy(dx: 0.0, dy: 0.0)
|
||||
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.size.width, height: contentFrame.size.height + 2000.0))
|
||||
if backgroundFrame.minY < contentFrame.minY {
|
||||
backgroundFrame.origin.y = contentFrame.minY
|
||||
}
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let titleSize = self.titleNode.measure(CGSize(width: width, height: titleAreaHeight))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: 15.0), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: 17.0, y: 17.0), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
//transition.updateFrame(node: self.closeButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight)))
|
||||
@ -177,4 +221,205 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
|
||||
}
|
||||
|
||||
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
|
||||
if let contentNode = self.contentNode {
|
||||
for item in contentNode.currentLayout.items {
|
||||
let frame = contentNode.effectiveFrameForItem(item)
|
||||
if frame.contains(point) {
|
||||
if item is InstantPagePeerReferenceItem {
|
||||
return .fail
|
||||
} else if item is InstantPageAudioItem {
|
||||
return .fail
|
||||
} else if item is InstantPageArticleItem {
|
||||
return .fail
|
||||
} else if item is InstantPageFeedbackItem {
|
||||
return .fail
|
||||
}
|
||||
if !(item is InstantPageImageItem || item is InstantPagePlayableVideoItem) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .waitForSingleTap
|
||||
}
|
||||
|
||||
private func updateTouchesAtPoint(_ location: CGPoint?) {
|
||||
var rects: [CGRect]?
|
||||
if let contentNode = self.contentNode, let location = location {
|
||||
for item in contentNode.currentLayout.items {
|
||||
let itemFrame = contentNode.effectiveFrameForItem(item)
|
||||
if itemFrame.contains(location) {
|
||||
var contentOffset = CGPoint()
|
||||
if let item = item as? InstantPageScrollableItem {
|
||||
contentOffset = contentNode.scrollableContentOffset(item: item)
|
||||
}
|
||||
var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY))
|
||||
for i in 0 ..< itemRects.count {
|
||||
itemRects[i] = itemRects[i].offsetBy(dx: itemFrame.minX - contentOffset.x + contentNode.frame.minX, dy: itemFrame.minY + contentNode.frame.minY).insetBy(dx: -2.0, dy: -2.0)
|
||||
}
|
||||
if !itemRects.isEmpty {
|
||||
rects = itemRects
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: self.theme.linkHighlightColor)
|
||||
linkHighlightingNode.isUserInteractionEnabled = false
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.contentContainerNode.addSubnode(linkHighlightingNode)
|
||||
}
|
||||
linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.contentContainerNode.bounds.size)
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
|
||||
if let contentNode = self.contentNode {
|
||||
for item in contentNode.currentLayout.items {
|
||||
let itemFrame = contentNode.effectiveFrameForItem(item)
|
||||
if itemFrame.contains(location) {
|
||||
if let item = item as? InstantPageTextItem, item.selectable {
|
||||
return (item, CGPoint(x: itemFrame.minX - item.frame.minX + contentNode.frame.minX, y: itemFrame.minY - item.frame.minY + contentNode.frame.minY))
|
||||
} else if let item = item as? InstantPageScrollableItem {
|
||||
let contentOffset = contentNode.scrollableContentOffset(item: item)
|
||||
if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
|
||||
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x + contentNode.frame.minX, dy: parentOffset.y + contentNode.frame.minY))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? {
|
||||
if let contentNode = self.contentNode, let (item, parentOffset) = self.textItemAtLocation(location) {
|
||||
return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX - parentOffset.x + contentNode.frame.minX, dy: -item.frame.minY - parentOffset.y + contentNode.frame.minY))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
guard let contentNode = self.contentNode else {
|
||||
return
|
||||
}
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
let location = location.offsetBy(dx: -contentNode.frame.minX, dy: -contentNode.frame.minY)
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if let url = self.urlForTapLocation(location) {
|
||||
self.close?()
|
||||
self.openUrl(url)
|
||||
}
|
||||
case .longTap:
|
||||
if let url = self.urlForTapLocation(location) {
|
||||
let canOpenIn = availableOpenInOptions(applicationContext: self.account.telegramApplicationContext, item: .url(url: url.url)).count > 1
|
||||
let openText = canOpenIn ? self.presentationData.strings.Conversation_FileOpenIn : self.presentationData.strings.Conversation_LinkDialogOpen
|
||||
let actionSheet = ActionSheetController(instantPageTheme: self.theme)
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: url.url),
|
||||
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.close?()
|
||||
if canOpenIn {
|
||||
strongSelf.openUrlIn(url)
|
||||
} else {
|
||||
strongSelf.openUrl(url)
|
||||
}
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = url.url
|
||||
}),
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let link = URL(string: url.url) {
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
self.present(actionSheet, nil)
|
||||
} else if let (item, parentOffset) = self.textItemAtLocation(location) {
|
||||
let textFrame = item.frame
|
||||
var itemRects = item.lineRects()
|
||||
for i in 0 ..< itemRects.count {
|
||||
itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0)
|
||||
}
|
||||
self.updateTextSelectionRects(itemRects, text: item.plainText())
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTextSelectionRects(_ rects: [CGRect], text: String?) {
|
||||
if let text = text, !rects.isEmpty {
|
||||
let textSelectionNode: LinkHighlightingNode
|
||||
if let current = self.textSelectionNode {
|
||||
textSelectionNode = current
|
||||
} else {
|
||||
textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4))
|
||||
textSelectionNode.isUserInteractionEnabled = false
|
||||
self.textSelectionNode = textSelectionNode
|
||||
self.contentContainerNode.addSubnode(textSelectionNode)
|
||||
}
|
||||
textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.contentContainerNode.bounds.size)
|
||||
textSelectionNode.updateRects(rects)
|
||||
|
||||
var coveringRect = rects[0]
|
||||
for i in 1 ..< rects.count {
|
||||
coveringRect = coveringRect.union(rects[i])
|
||||
}
|
||||
|
||||
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuCopy), action: {
|
||||
UIPasteboard.general.string = text
|
||||
}), ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
||||
if let strongSelf = self, case let .Loaded(content) = strongSelf.webPage.content {
|
||||
strongSelf.present(ShareController(account: strongSelf.account, subject: .quote(text: text, url: content.url)), nil)
|
||||
}
|
||||
})])
|
||||
controller.dismissed = { [weak self] in
|
||||
self?.updateTextSelectionRects([], text: nil)
|
||||
}
|
||||
self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
return (strongSelf.contentContainerNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf, strongSelf.bounds)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}))
|
||||
textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
} else if let textSelectionNode = self.textSelectionNode {
|
||||
self.textSelectionNode = nil
|
||||
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
|
||||
textSelectionNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
75
TelegramUI/InstantPageStoredState.swift
Normal file
75
TelegramUI/InstantPageStoredState.swift
Normal file
@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
final class InstantPageStoredDetailsState: PostboxCoding {
|
||||
let index: Int32
|
||||
let expanded: Bool
|
||||
let details: [InstantPageStoredDetailsState]
|
||||
|
||||
init(index: Int32, expanded: Bool, details: [InstantPageStoredDetailsState]) {
|
||||
self.index = index
|
||||
self.expanded = expanded
|
||||
self.details = details
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.index = decoder.decodeInt32ForKey("index", orElse: 0)
|
||||
self.expanded = decoder.decodeBoolForKey("expanded", orElse: false)
|
||||
self.details = decoder.decodeObjectArrayForKey("details")
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.index, forKey: "index")
|
||||
encoder.encodeBool(self.expanded, forKey: "expanded")
|
||||
encoder.encodeObjectArray(self.details, forKey: "details")
|
||||
}
|
||||
}
|
||||
|
||||
final class InstantPageStoredState: PostboxCoding {
|
||||
let contentOffset: Double
|
||||
let details: [InstantPageStoredDetailsState]
|
||||
|
||||
init(contentOffset: Double, details: [InstantPageStoredDetailsState]) {
|
||||
self.contentOffset = contentOffset
|
||||
self.details = details
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.contentOffset = decoder.decodeDoubleForKey("offset", orElse: 0.0)
|
||||
self.details = decoder.decodeObjectArrayForKey("details")
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeDouble(self.contentOffset, forKey: "offset")
|
||||
encoder.encodeObjectArray(self.details, forKey: "details")
|
||||
}
|
||||
}
|
||||
|
||||
func instantPageStoredState(postbox: Postbox, webPage: TelegramMediaWebpage) -> Signal<InstantPageStoredState?, NoError> {
|
||||
return postbox.transaction { transaction -> InstantPageStoredState? in
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: webPage.webpageId.id)
|
||||
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, key: key)) as? InstantPageStoredState {
|
||||
return entry
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200)
|
||||
|
||||
func updateInstantPageStoredStateInteractively(postbox: Postbox, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal<Void, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: webPage.webpageId.id)
|
||||
let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, key: key)
|
||||
if let state = state {
|
||||
transaction.putItemCacheEntry(id: id, entry: state, collectionSpec: collectionSpec)
|
||||
} else {
|
||||
transaction.removeItemCacheEntry(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -711,13 +711,13 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
|
||||
}
|
||||
|
||||
var height: CGFloat = 0.0
|
||||
if !lines.isEmpty && !(string.length == 1 && hasAnchors) {
|
||||
if !lines.isEmpty && !(string.string == "\u{200b}" && hasAnchors) {
|
||||
height = lines.last!.frame.maxY + extraDescent
|
||||
}
|
||||
|
||||
var textWidth = boundingWidth
|
||||
var requiresScroll = false
|
||||
if maxLineWidth > boundingWidth + 10.0 {
|
||||
if !imageItems.isEmpty && maxLineWidth > boundingWidth + 10.0 {
|
||||
textWidth = maxLineWidth
|
||||
requiresScroll = true
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import Display
|
||||
|
||||
enum InstantPageFontStyle {
|
||||
case sans
|
||||
@ -80,6 +81,7 @@ struct InstantPageTextCategories {
|
||||
}
|
||||
|
||||
final class InstantPageTheme {
|
||||
let type: InstantPageThemeType
|
||||
let pageBackgroundColor: UIColor
|
||||
|
||||
let textCategories: InstantPageTextCategories
|
||||
@ -100,12 +102,14 @@ final class InstantPageTheme {
|
||||
|
||||
let tableBorderColor: UIColor
|
||||
let tableHeaderColor: UIColor
|
||||
|
||||
let controlColor: UIColor
|
||||
|
||||
let imageTintColor: UIColor?
|
||||
|
||||
init(pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, serif: Bool, codeBlockBackgroundColor: UIColor, linkColor: UIColor, textHighlightColor: UIColor, linkHighlightColor: UIColor, markerColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor, tableBorderColor: UIColor, tableHeaderColor: UIColor, controlColor: UIColor, imageTintColor: UIColor?) {
|
||||
let overlayPanelColor: UIColor
|
||||
|
||||
init(type: InstantPageThemeType, pageBackgroundColor: UIColor, textCategories: InstantPageTextCategories, serif: Bool, codeBlockBackgroundColor: UIColor, linkColor: UIColor, textHighlightColor: UIColor, linkHighlightColor: UIColor, markerColor: UIColor, panelBackgroundColor: UIColor, panelHighlightedBackgroundColor: UIColor, panelPrimaryColor: UIColor, panelSecondaryColor: UIColor, panelAccentColor: UIColor, tableBorderColor: UIColor, tableHeaderColor: UIColor, controlColor: UIColor, imageTintColor: UIColor?, overlayPanelColor: UIColor) {
|
||||
self.type = type
|
||||
self.pageBackgroundColor = pageBackgroundColor
|
||||
self.textCategories = textCategories
|
||||
self.serif = serif
|
||||
@ -123,14 +127,16 @@ final class InstantPageTheme {
|
||||
self.tableHeaderColor = tableHeaderColor
|
||||
self.controlColor = controlColor
|
||||
self.imageTintColor = imageTintColor
|
||||
self.overlayPanelColor = overlayPanelColor
|
||||
}
|
||||
|
||||
func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme {
|
||||
return InstantPageTheme(pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor)
|
||||
return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor)
|
||||
}
|
||||
}
|
||||
|
||||
private let lightTheme = InstantPageTheme(
|
||||
type: .light,
|
||||
pageBackgroundColor: .white,
|
||||
textCategories: InstantPageTextCategories(
|
||||
kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: .black),
|
||||
@ -156,10 +162,12 @@ private let lightTheme = InstantPageTheme(
|
||||
tableBorderColor: UIColor(rgb: 0xe2e2e2),
|
||||
tableHeaderColor: UIColor(rgb: 0xf4f4f4),
|
||||
controlColor: UIColor(rgb: 0xc7c7cd),
|
||||
imageTintColor: nil
|
||||
imageTintColor: nil,
|
||||
overlayPanelColor: .white
|
||||
)
|
||||
|
||||
private let sepiaTheme = InstantPageTheme(
|
||||
type: .sepia,
|
||||
pageBackgroundColor: UIColor(rgb: 0xf8f1e2),
|
||||
textCategories: InstantPageTextCategories(
|
||||
kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)),
|
||||
@ -185,10 +193,12 @@ private let sepiaTheme = InstantPageTheme(
|
||||
tableBorderColor: UIColor(rgb: 0xddd1b8),
|
||||
tableHeaderColor: UIColor(rgb: 0xf0e7d4),
|
||||
controlColor: UIColor(rgb: 0xddd1b8),
|
||||
imageTintColor: nil
|
||||
imageTintColor: nil,
|
||||
overlayPanelColor: UIColor(rgb: 0xf8f1e2)
|
||||
)
|
||||
|
||||
private let grayTheme = InstantPageTheme(
|
||||
type: .gray,
|
||||
pageBackgroundColor: UIColor(rgb: 0x5a5a5c),
|
||||
textCategories: InstantPageTextCategories(
|
||||
kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)),
|
||||
@ -214,10 +224,12 @@ private let grayTheme = InstantPageTheme(
|
||||
tableBorderColor: UIColor(rgb: 0x484848),
|
||||
tableHeaderColor: UIColor(rgb: 0x555556),
|
||||
controlColor: UIColor(rgb: 0x484848),
|
||||
imageTintColor: UIColor(rgb: 0xcecece)
|
||||
imageTintColor: UIColor(rgb: 0xcecece),
|
||||
overlayPanelColor: UIColor(rgb: 0x5a5a5c)
|
||||
)
|
||||
|
||||
private let darkTheme = InstantPageTheme(
|
||||
type: .dark,
|
||||
pageBackgroundColor: UIColor(rgb: 0x000000),
|
||||
textCategories: InstantPageTextCategories(
|
||||
kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)),
|
||||
@ -243,7 +255,8 @@ private let darkTheme = InstantPageTheme(
|
||||
tableBorderColor: UIColor(rgb: 0x303030),
|
||||
tableHeaderColor: UIColor(rgb: 0x131313),
|
||||
controlColor: UIColor(rgb: 0x303030),
|
||||
imageTintColor: UIColor(rgb: 0xb0b0b0)
|
||||
imageTintColor: UIColor(rgb: 0xb0b0b0),
|
||||
overlayPanelColor: UIColor(rgb: 0x232323)
|
||||
)
|
||||
|
||||
private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFontSize) -> CGFloat {
|
||||
@ -307,3 +320,15 @@ func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantPage
|
||||
return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(settings.fontSize), forceSerif: settings.forceSerif)
|
||||
}
|
||||
}
|
||||
|
||||
extension ActionSheetControllerTheme {
|
||||
convenience init(instantPageTheme: InstantPageTheme) {
|
||||
self.init(dimColor: UIColor(white: 0.0, alpha: 0.4), backgroundType: instantPageTheme.type != .dark ? .light : .dark, itemBackgroundColor: instantPageTheme.overlayPanelColor, itemHighlightedBackgroundColor: instantPageTheme.panelHighlightedBackgroundColor, standardActionTextColor: instantPageTheme.panelAccentColor, destructiveActionTextColor: instantPageTheme.panelAccentColor, disabledActionTextColor: instantPageTheme.panelAccentColor, primaryTextColor: instantPageTheme.textCategories.paragraph.color, secondaryTextColor: instantPageTheme.textCategories.caption.color, controlAccentColor: instantPageTheme.panelAccentColor)
|
||||
}
|
||||
}
|
||||
|
||||
extension ActionSheetController {
|
||||
convenience init(instantPageTheme: InstantPageTheme) {
|
||||
self.init(theme: ActionSheetControllerTheme(instantPageTheme: instantPageTheme))
|
||||
}
|
||||
}
|
||||
|
||||
11
TelegramUI/ItemCacheKeys.swift
Normal file
11
TelegramUI/ItemCacheKeys.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
|
||||
private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||
case instantPageStoredState = 0
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificItemCacheCollectionId {
|
||||
public static let instantPageStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.instantPageStoredState.rawValue)
|
||||
}
|
||||
@ -9,21 +9,22 @@ public enum NavigateToChatKeepStack {
|
||||
case never
|
||||
}
|
||||
|
||||
public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, animated: Bool = true, completion: @escaping () -> Void = {}) {
|
||||
public func navigateToChatController(navigationController: NavigationController, chatController: ChatController? = nil, account: Account, chatLocation: ChatLocation, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, animated: Bool = true, completion: @escaping () -> Void = {}) {
|
||||
var found = false
|
||||
var isFirst = true
|
||||
for controller in navigationController.viewControllers.reversed() {
|
||||
if let controller = controller as? ChatController, controller.chatLocation == chatLocation {
|
||||
if let messageId = messageId {
|
||||
controller.purposefulAction = purposefulAction
|
||||
controller.navigateToMessage(messageLocation: .id(messageId), animated: isFirst, completion: { [weak navigationController, weak controller] in
|
||||
if let navigationController = navigationController, let controller = controller {
|
||||
let _ = navigationController.popToViewController(controller, animated: animated)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let _ = navigationController.popToViewController(controller, animated: animated)
|
||||
} else if scrollToEndIfExists && isFirst {
|
||||
controller.scrollToEndOfHistory()
|
||||
}
|
||||
controller.purposefulAction = purposefulAction
|
||||
let _ = navigationController.popToViewController(controller, animated: animated)
|
||||
completion()
|
||||
found = true
|
||||
break
|
||||
|
||||
@ -75,6 +75,33 @@ final class ApplicationSpecificCounterNotice: NoticeEntry {
|
||||
}
|
||||
}
|
||||
|
||||
final class ApplicationSpecificTimestampNotice: NoticeEntry {
|
||||
let value: Int32
|
||||
|
||||
init(value: Int32) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.value = decoder.decodeInt32ForKey("v", orElse: 0)
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.value, forKey: "v")
|
||||
}
|
||||
|
||||
func isEqual(to: NoticeEntry) -> Bool {
|
||||
if let to = to as? ApplicationSpecificTimestampNotice {
|
||||
if self.value != to.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func noticeNamespace(namespace: Int32) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: namespace)
|
||||
@ -106,6 +133,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
||||
private struct ApplicationSpecificNoticeKeys {
|
||||
private static let botPaymentLiabilityNamespace: Int32 = 1
|
||||
private static let globalNamespace: Int32 = 2
|
||||
private static let permissionsNamespace: Int32 = 3
|
||||
|
||||
static func botPaymentLiabilityNotice(peerId: PeerId) -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: botPaymentLiabilityNamespace), key: noticeKey(peerId: peerId, key: 0))
|
||||
|
||||
@ -572,16 +572,16 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound
|
||||
private func notificationsAndSoundsEntries(authorizationStatus: AccessType, globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode), presentationData: PresentationData) -> [NotificationsAndSoundsEntry] {
|
||||
var entries: [NotificationsAndSoundsEntry] = []
|
||||
|
||||
switch authorizationStatus {
|
||||
case .denied:
|
||||
entries.append(.permissionInfo(presentationData.theme, presentationData.strings))
|
||||
entries.append(.permissionEnable(presentationData.theme, "Turn ON in Settings"))
|
||||
case .notDetermined:
|
||||
entries.append(.permissionInfo(presentationData.theme, presentationData.strings))
|
||||
entries.append(.permissionEnable(presentationData.theme, "Turn Notifications ON"))
|
||||
default:
|
||||
break
|
||||
}
|
||||
// switch authorizationStatus {
|
||||
// case .denied:
|
||||
// entries.append(.permissionInfo(presentationData.theme, presentationData.strings))
|
||||
// entries.append(.permissionEnable(presentationData.theme, "Turn ON in Settings"))
|
||||
// case .notDetermined:
|
||||
// entries.append(.permissionInfo(presentationData.theme, presentationData.strings))
|
||||
// entries.append(.permissionEnable(presentationData.theme, "Turn Notifications ON"))
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
|
||||
entries.append(.messageHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications))
|
||||
entries.append(.messageAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.privateChats.enabled))
|
||||
|
||||
96
TelegramUI/PermissionContentNode.swift
Normal file
96
TelegramUI/PermissionContentNode.swift
Normal file
@ -0,0 +1,96 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class PermissionContentNode: ASDisplayNode {
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let actionButton: SolidRoundedButtonNode
|
||||
private let privacyPolicyButton: HighlightableButtonNode
|
||||
|
||||
var kind: PermissionStateKind
|
||||
private var title: String
|
||||
|
||||
var buttonAction: (() -> Void)?
|
||||
var openPrivacyPolicy: (() -> Void)?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, kind: PermissionStateKind, icon: UIImage?, title: String, text: String, buttonTitle: String, buttonAction: @escaping () -> Void, openPrivacyPolicy: (() -> Void)?) {
|
||||
self.kind = kind
|
||||
|
||||
self.buttonAction = buttonAction
|
||||
self.openPrivacyPolicy = openPrivacyPolicy
|
||||
|
||||
self.title = title
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 0
|
||||
self.titleNode.textAlignment = .center
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.textAlignment = .center
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
self.actionButton = SolidRoundedButtonNode(theme: theme, height: 48.0, cornerRadius: 9.0)
|
||||
|
||||
self.privacyPolicyButton = HighlightableButtonNode()
|
||||
//self.privacyPolicyButton.setTitle(strings.Permissions_PrivacyPolicy, with: Font.regular(16.0), with: theme.list.itemAccentColor, for: .normal)
|
||||
|
||||
super.init()
|
||||
|
||||
self.iconNode.image = icon
|
||||
self.title = title
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemPrimaryTextColor)
|
||||
let link = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""])
|
||||
self.textNode.attributedText = parseMarkdownIntoAttributedString(text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center)
|
||||
|
||||
self.actionButton.title = buttonTitle
|
||||
self.privacyPolicyButton.isHidden = openPrivacyPolicy != nil
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.actionButton)
|
||||
self.addSubnode(self.privacyPolicyButton)
|
||||
|
||||
self.actionButton.pressed = { [weak self] in
|
||||
self?.buttonAction?()
|
||||
}
|
||||
|
||||
self.privacyPolicyButton.addTarget(self, action: #selector(self.privacyPolicyPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func privacyPolicyPressed() {
|
||||
self.openPrivacyPolicy?()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||
let sidePadding: CGFloat = 20.0
|
||||
//let sideButtonInset: CGFloat = 16.0
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude))
|
||||
let buttonHeight = self.actionButton.updateLayout(width: size.width, transition: transition)
|
||||
|
||||
let titleSubtitleSpacing: CGFloat = 12.0
|
||||
|
||||
let textHeight = titleSize.height + titleSubtitleSpacing + textSize.height
|
||||
|
||||
|
||||
let minContentHeight = textHeight
|
||||
let contentHeight = min(215.0, max(size.height - insets.top - insets.bottom - 40.0, minContentHeight))
|
||||
let contentOrigin = insets.top + floor((size.height - insets.top - insets.bottom - contentHeight) / 2.0)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentOrigin), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSubtitleSpacing), size: textSize))
|
||||
transition.updateFrame(node: self.actionButton, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: buttonHeight))
|
||||
}
|
||||
}
|
||||
@ -5,46 +5,89 @@ import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
final class PermissionController : ViewController {
|
||||
private let account: Account
|
||||
|
||||
private var controllerNode: PermissionControllerNode {
|
||||
return self.displayNode as! PermissionControllerNode
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
private let strings: PresentationStrings
|
||||
private let theme: AuthorizationTheme
|
||||
private var _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private var animatedIn = false
|
||||
private var didPlayPresentationAnimation = false
|
||||
|
||||
private var allow: (() -> Void)?
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
self.strings = account.telegramApplicationContext.currentPresentationData.with { $0 }.strings
|
||||
self.theme = defaultLightAuthorizationTheme
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
self.statusBar.statusBarStyle = self.theme.statusBarStyle
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
|
||||
self.presentationDataDisposable = (account.telegramApplicationContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
let previousTheme = strongSelf.presentationData.theme
|
||||
let previousStrings = strongSelf.presentationData.strings
|
||||
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
||||
strongSelf.updateThemeAndStrings()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
|
||||
self.didPlayPresentationAnimation = true
|
||||
if case .modalSheet = presentationArguments.presentationAnimation {
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
completion?()
|
||||
})
|
||||
self.view.endEditing(true)
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
self.title = self.presentationData.strings.Settings_AppLanguage
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
|
||||
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Permissions_Skip, style: .plain, target: self, action: #selector(PermissionController.nextPressed))
|
||||
self.controllerNode.updatePresentationData(self.presentationData)
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = PermissionControllerNode(theme: self.theme, strings: self.strings)
|
||||
self.displayNode = PermissionControllerNode(account: self.account)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
self.controllerNode.allow = { [weak self] in
|
||||
self?.allow?()
|
||||
}
|
||||
self.controllerNode.next = { [weak self] in
|
||||
self?.dismiss(completion: nil)
|
||||
//self?.allow?()
|
||||
}
|
||||
self.controllerNode.openPrivacyPolicy = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
@ -53,27 +96,13 @@ final class PermissionController : ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedIn {
|
||||
self.animatedIn = true
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override func dismiss(completion: (() -> Void)?) {
|
||||
self.controllerNode.animateOut(completion: completion)
|
||||
}
|
||||
|
||||
func updateData(subject: DeviceAccessSubject, currentStatus: AccessType, allow: @escaping () -> Void) {
|
||||
self.allow = allow
|
||||
self.controllerNode.updateData(subject: .notifications, currentStatus: currentStatus)
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
@objc private func nextPressed() {
|
||||
//self.controllerNode.activateNextAction()
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,58 +4,88 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
final class PermissionControllerNode: ASDisplayNode {
|
||||
private let theme: AuthorizationTheme
|
||||
private let strings: PresentationStrings
|
||||
enum PermissionStateKind: Int32 {
|
||||
case contacts
|
||||
case notifications
|
||||
case siri
|
||||
case cellularData
|
||||
}
|
||||
|
||||
private enum PermissionRequestStatus {
|
||||
case requestable
|
||||
case denied
|
||||
}
|
||||
|
||||
private enum PermissionNotificationsRequestStatus {
|
||||
case requestable
|
||||
case denied
|
||||
case unreachable
|
||||
}
|
||||
|
||||
private enum PermissionState: Equatable {
|
||||
case contacts(status: PermissionRequestStatus)
|
||||
case notifications(status: PermissionNotificationsRequestStatus)
|
||||
case siri(status: PermissionRequestStatus)
|
||||
case cellularData
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let buttonNode: SolidRoundedButtonNode
|
||||
private let privacyPolicyNode: HighlightableButtonNode
|
||||
private let nextNode: HighlightableButtonNode
|
||||
|
||||
private var layoutArguments: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private var title: String?
|
||||
|
||||
var allow: (() -> Void)?
|
||||
var next: (() -> Void)? {
|
||||
didSet {
|
||||
self.nextNode.isHidden = self.next == nil
|
||||
var kind: PermissionStateKind {
|
||||
switch self {
|
||||
case .contacts:
|
||||
return .contacts
|
||||
case .notifications:
|
||||
return .notifications
|
||||
case .siri:
|
||||
return .siri
|
||||
case .cellularData:
|
||||
return .cellularData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PermissionControllerDataState: Equatable {
|
||||
var state: PermissionState?
|
||||
}
|
||||
|
||||
private struct PermissionControllerLayoutState: Equatable {
|
||||
let layout: ContainerViewLayout
|
||||
let navigationHeight: CGFloat
|
||||
}
|
||||
|
||||
private struct PermissionControllerInnerState: Equatable {
|
||||
var layout: PermissionControllerLayoutState?
|
||||
var data: PermissionControllerDataState
|
||||
}
|
||||
|
||||
private struct PermissionControllerState: Equatable {
|
||||
var layout: PermissionControllerLayoutState
|
||||
var data: PermissionControllerDataState
|
||||
}
|
||||
|
||||
extension PermissionControllerState {
|
||||
init?(_ state: PermissionControllerInnerState) {
|
||||
guard let layout = state.layout else {
|
||||
return nil
|
||||
}
|
||||
self.init(layout: layout, data: state.data)
|
||||
}
|
||||
}
|
||||
|
||||
final class PermissionControllerNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private var innerState: PermissionControllerInnerState
|
||||
|
||||
private var contentNode: PermissionContentNode?
|
||||
|
||||
var allow: (() -> Void)?
|
||||
var openPrivacyPolicy: (() -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
|
||||
init(theme: AuthorizationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 0
|
||||
self.titleNode.textAlignment = .center
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.textAlignment = .center
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
self.buttonNode = SolidRoundedButtonNode(theme: self.theme, height: 48.0, cornerRadius: 9.0)
|
||||
|
||||
self.privacyPolicyNode = HighlightableButtonNode()
|
||||
self.privacyPolicyNode.setTitle("Privacy Policy", with: Font.regular(16.0), with: self.theme.accentColor, for: .normal)
|
||||
|
||||
self.nextNode = HighlightableButtonNode()
|
||||
self.nextNode.setTitle("Skip", with: Font.regular(17.0), with: self.theme.accentColor, for: .normal)
|
||||
self.nextNode.isHidden = true
|
||||
init(account: Account) {
|
||||
self.account = account
|
||||
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
||||
self.innerState = PermissionControllerInnerState(layout: nil, data: PermissionControllerDataState(state: nil))
|
||||
|
||||
super.init()
|
||||
|
||||
@ -63,119 +93,124 @@ final class PermissionControllerNode: ASDisplayNode {
|
||||
return UITracingLayerView()
|
||||
})
|
||||
|
||||
self.backgroundColor = self.theme.backgroundColor
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.privacyPolicyNode)
|
||||
self.addSubnode(self.nextNode)
|
||||
|
||||
self.buttonNode.pressed = { [weak self] in
|
||||
self?.allow?()
|
||||
self?.dismiss?()
|
||||
}
|
||||
|
||||
self.privacyPolicyNode.addTarget(self, action: #selector(self.privacyPolicyPressed), forControlEvents: .touchUpInside)
|
||||
self.nextNode.addTarget(self, action: #selector(self.nextPressed), forControlEvents: .touchUpInside)
|
||||
self.applyPresentationData()
|
||||
}
|
||||
|
||||
func updateData(subject: DeviceAccessSubject, currentStatus: AccessType) {
|
||||
var icon: UIImage?
|
||||
var title = ""
|
||||
var text = ""
|
||||
var buttonTitle = ""
|
||||
var hasPrivacyPolicy = false
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
switch subject {
|
||||
case .contacts:
|
||||
icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
|
||||
title = "Sync Your Contacts"
|
||||
text = "See who's on Telegram and switch seamlessly, without having to \"add\" to add your friends."
|
||||
if currentStatus == .denied {
|
||||
buttonTitle = "Allow in Settings"
|
||||
} else {
|
||||
buttonTitle = "Allow Access"
|
||||
}
|
||||
hasPrivacyPolicy = true
|
||||
case .notifications:
|
||||
icon = UIImage(bundleImageName: "Settings/Permissions/Notifications")
|
||||
title = "Turn ON Notifications"
|
||||
text = "Don't miss important messages from your friends and coworkers."
|
||||
if currentStatus == .denied || currentStatus == .restricted {
|
||||
buttonTitle = "Turn ON in Settings"
|
||||
} else {
|
||||
buttonTitle = "Turn Notifications ON"
|
||||
}
|
||||
case .cellularData:
|
||||
icon = UIImage(bundleImageName: "Settings/Permissions/CellularData")
|
||||
title = "Turn ON Mobile Data"
|
||||
text = "Don't worry, Telegram keeps network usage to a minimum. You can further control this in Settings > Data and Storage."
|
||||
buttonTitle = "Turn ON in Settings"
|
||||
case .siri:
|
||||
title = "Turn ON Siri"
|
||||
text = "Use Siri to send messages and make calls."
|
||||
if currentStatus == .denied {
|
||||
buttonTitle = "Turn ON in Settings"
|
||||
} else {
|
||||
buttonTitle = "Turn Siri ON"
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self.iconNode.image = icon
|
||||
self.title = title
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.primaryColor)
|
||||
let link = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.theme.accentColor, additionalAttributes: [TelegramTextAttributes.URL: ""])
|
||||
self.textNode.attributedText = parseMarkdownIntoAttributedString(text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center)
|
||||
|
||||
self.buttonNode.title = buttonTitle
|
||||
|
||||
self.privacyPolicyNode.isHidden = !hasPrivacyPolicy
|
||||
|
||||
if let (layout, navigationHeight) = self.layoutArguments {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||
self.applyPresentationData()
|
||||
}
|
||||
|
||||
private func applyPresentationData() {
|
||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor }
|
||||
|
||||
func animateIn(completion: (() -> Void)? = nil) {
|
||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)? = nil) {
|
||||
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
private func updateState(_ f: (PermissionControllerInnerState) -> PermissionControllerInnerState, transition: ContainedViewLayoutTransition) {
|
||||
let updatedState = f(self.innerState)
|
||||
if updatedState != self.innerState {
|
||||
self.innerState = updatedState
|
||||
if let state = PermissionControllerState(updatedState) {
|
||||
self.transition(state: state, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func transition(state: PermissionControllerState, transition: ContainedViewLayoutTransition) {
|
||||
// let insets = state.layout.layout.insets(options: [.statusBar])
|
||||
// let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height))
|
||||
//
|
||||
// if state.data.state?.kind != self.contentNode?.kind {
|
||||
// if let dataState = state.data.state {
|
||||
// let icon: UIImage?
|
||||
// let title: String
|
||||
// let text: String
|
||||
// let buttonTitle: String
|
||||
// let hasPrivacyPolicy: Bool
|
||||
//
|
||||
// switch dataState {
|
||||
// case let .contacts(status):
|
||||
// icon = UIImage(bundleImageName: "Settings/Permissions/Contacts")
|
||||
// title = self.presentationData.strings.Permissions_ContactsTitle
|
||||
// text = self.presentationData.strings.Permissions_ContactsText
|
||||
// if status == .denied {
|
||||
// buttonTitle = self.presentationData.strings.Permissions_ContactsAllowInSettings
|
||||
// } else {
|
||||
// buttonTitle = self.presentationData.strings.Permissions_ContactsAllow
|
||||
// }
|
||||
// hasPrivacyPolicy = true
|
||||
// case let .notifications(status):
|
||||
// icon = UIImage(bundleImageName: "Settings/Permissions/Notifications")
|
||||
// title = self.presentationData.strings.Permissions_NotificationsTitle
|
||||
// text = self.presentationData.strings.Permissions_NotificationsText
|
||||
// if status == .denied {
|
||||
// buttonTitle = self.presentationData.strings.Permissions_NotificationsAllowInSettings
|
||||
// } else {
|
||||
// buttonTitle = self.presentationData.strings.Permissions_NotificationsAllow
|
||||
// }
|
||||
// hasPrivacyPolicy = false
|
||||
// case let .siri(status):
|
||||
// icon = UIImage(bundleImageName: "Settings/Permissions/CellularData")
|
||||
// title = self.presentationData.strings.Permissions_SiriTitle
|
||||
// text = self.presentationData.strings.Permissions_SiriText
|
||||
// if status == .denied {
|
||||
// buttonTitle = self.presentationData.strings.Permissions_SiriAllowInSettings
|
||||
// } else {
|
||||
// buttonTitle = self.presentationData.strings.Permissions_SiriAllow
|
||||
// }
|
||||
// hasPrivacyPolicy = false
|
||||
// case .cellularData:
|
||||
// icon = UIImage(bundleImageName: "Settings/Permissions/CellularData")
|
||||
// title = self.presentationData.strings.Permissions_CellularDataTitle
|
||||
// text = self.presentationData.strings.Permissions_CellularDataText
|
||||
// buttonTitle = self.presentationData.strings.Permissions_CellularDataAllowInSettings
|
||||
// hasPrivacyPolicy = false
|
||||
// }
|
||||
//
|
||||
// let contentNode = PermissionContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, kind: dataState.kind, icon: icon, title: title, text: text, buttonTitle: buttonTitle, buttonAction: {}, openPrivacyPolicy: hasPrivacyPolicy ? self.openPrivacyPolicy : nil)
|
||||
// self.insertSubnode(contentNode, at: 0)
|
||||
// contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: .immediate)
|
||||
// contentNode.frame = contentFrame
|
||||
// if let currentContentNode = self.contentNode {
|
||||
// transition.updatePosition(node: currentContentNode, position: CGPoint(x: -contentFrame.size.width / 2.0, y: contentFrame.midY), completion: { [weak currentContentNode] _ in
|
||||
// currentContentNode?.removeFromSupernode()
|
||||
// })
|
||||
// transition.animateHorizontalOffsetAdditive(node: contentNode, offset: -contentFrame.width)
|
||||
// } else if transition.isAnimated {
|
||||
// contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
// }
|
||||
// self.contentNode = contentNode
|
||||
// } else if let currentContentNode = self.contentNode {
|
||||
// transition.updateAlpha(node: currentContentNode, alpha: 0.0, completion: { [weak currentContentNode] _ in
|
||||
// currentContentNode?.removeFromSupernode()
|
||||
// })
|
||||
// self.contentNode = nil
|
||||
// }
|
||||
// } else if let contentNode = self.contentNode {
|
||||
// transition.updateFrame(node: contentNode, frame: contentFrame)
|
||||
// contentNode.updateLayout(size: contentFrame.size, insets: insets, transition: transition)
|
||||
// }
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.layoutArguments = (layout, navigationBarHeight)
|
||||
self.updateState({ state in
|
||||
var state = state
|
||||
state.layout = PermissionControllerLayoutState(layout: layout, navigationHeight: navigationBarHeight)
|
||||
return state
|
||||
}, transition: transition)
|
||||
}
|
||||
|
||||
func activateNextAction() {
|
||||
|
||||
let insets = layout.insets(options: [.statusBar])
|
||||
let fontSize: CGFloat
|
||||
let sideInset: CGFloat
|
||||
if layout.size.width > 330.0 {
|
||||
fontSize = 22.0
|
||||
sideInset = 38.0
|
||||
} else {
|
||||
fontSize = 18.0
|
||||
sideInset = 20.0
|
||||
}
|
||||
|
||||
let nextSize = self.nextNode.measure(layout.size)
|
||||
transition.updateFrame(node: self.nextNode, frame: CGRect(x: layout.size.width - insets.right - nextSize.width - 16.0, y: insets.top + 10.0, width: nextSize.width, height: nextSize.height))
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(fontSize), textColor: self.theme.primaryColor)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let buttonHeight = self.buttonNode.updateLayout(width: layout.size.width, transition: transition)
|
||||
|
||||
var items: [AuthorizationLayoutItem] = []
|
||||
if let icon = self.iconNode.image {
|
||||
items.append(AuthorizationLayoutItem(node: self.iconNode, size: icon.size, spacingBefore: AuthorizationLayoutItemSpacing(weight: 122.0, maxValue: 122.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 15.0, maxValue: 15.0)))
|
||||
}
|
||||
items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)))
|
||||
items.append(AuthorizationLayoutItem(node: self.textNode, size: textSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 5.0, maxValue: 5.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 35.0, maxValue: 35.0)))
|
||||
items.append(AuthorizationLayoutItem(node: self.buttonNode, size: CGSize(width: layout.size.width, height: buttonHeight), spacingBefore: AuthorizationLayoutItemSpacing(weight: 35.0, maxValue: 35.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 50.0)))
|
||||
|
||||
let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 20.0)), items: items, transition: transition, failIfDoesNotFit: false)
|
||||
|
||||
let privacyPolicySize = self.privacyPolicyNode.measure(layout.size)
|
||||
transition.updateFrame(node: self.privacyPolicyNode, frame: CGRect(x: (layout.size.width - privacyPolicySize.width) / 2.0, y: self.buttonNode.frame.maxY + (layout.size.height - self.buttonNode.frame.maxY - insets.bottom - privacyPolicySize.height) / 2.0, width: privacyPolicySize.width, height: privacyPolicySize.height))
|
||||
}
|
||||
|
||||
@objc func allowPressed() {
|
||||
@ -185,21 +220,4 @@ final class PermissionControllerNode: ASDisplayNode {
|
||||
@objc func privacyPolicyPressed() {
|
||||
self.openPrivacyPolicy?()
|
||||
}
|
||||
|
||||
@objc func nextPressed() {
|
||||
self.next?()
|
||||
}
|
||||
|
||||
func animateIn(completion: (() -> Void)? = nil) {
|
||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)? = nil) {
|
||||
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss?()
|
||||
}
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,14 +90,9 @@ enum PresentationResourceKey: Int32 {
|
||||
|
||||
case chatBubbleMediaOverlayControlSecret
|
||||
|
||||
case chatLoadingIndicatorBackgroundImage
|
||||
case chatServiceBubbleFillImage
|
||||
|
||||
case chatBubbleSecretMediaIcon
|
||||
case chatBubbleSecretMediaCompactIcon
|
||||
|
||||
case chatFreeformContentAdditionalInfoBackgroundImage
|
||||
|
||||
case chatInstantVideoWithWallpaperBackgroundImage
|
||||
case chatInstantVideoWithoutWallpaperBackgroundImage
|
||||
|
||||
@ -126,7 +121,6 @@ enum PresentationResourceKey: Int32 {
|
||||
case chatInfoItemBackgroundImageWithWallpaper
|
||||
case chatInfoItemBackgroundImageWithoutWallpaper
|
||||
|
||||
case chatEmptyItemBackgroundImage
|
||||
case chatEmptyItemIconImage
|
||||
|
||||
case chatInputPanelCloseIconImage
|
||||
|
||||
@ -65,8 +65,9 @@ struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
static func principalGraphics(_ theme: PresentationTheme, wallpaper: Bool) -> PrincipalThemeEssentialGraphics {
|
||||
let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatPrincipalThemeEssentialGraphicsWithoutWallpaper : PresentationResourceKey.chatPrincipalThemeEssentialGraphicsWithWallpaper
|
||||
static func principalGraphics(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> PrincipalThemeEssentialGraphics {
|
||||
let hasWallpaper = !wallpaper.isEmpty && !wallpaper.isBuiltin
|
||||
let key: PresentationResourceKey = !hasWallpaper ? PresentationResourceKey.chatPrincipalThemeEssentialGraphicsWithoutWallpaper : PresentationResourceKey.chatPrincipalThemeEssentialGraphicsWithWallpaper
|
||||
return theme.object(key.rawValue, { theme in
|
||||
return PrincipalThemeEssentialGraphics(theme.chat, wallpaper: wallpaper)
|
||||
}) as! PrincipalThemeEssentialGraphics
|
||||
@ -86,7 +87,7 @@ struct PresentationResourcesChat {
|
||||
|
||||
static func chatServiceVerticalLineImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatServiceVerticalLineImage.rawValue, { theme in
|
||||
return generateLineImage(color: theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
return generateLineImage(color: theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText)
|
||||
})
|
||||
}
|
||||
|
||||
@ -168,16 +169,6 @@ struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
static func chatServiceBubbleFillImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatServiceBubbleFillImage.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.chat.serviceMessage.serviceMessageFillColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
|
||||
})
|
||||
}
|
||||
|
||||
static func chatBubbleSecretMediaIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleSecretMediaIcon.rawValue, { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.bubble.mediaOverlayControlForegroundColor)
|
||||
@ -198,12 +189,6 @@ struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
static func chatFreeformContentAdditionalInfoBackgroundImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatFreeformContentAdditionalInfoBackgroundImage.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(radius: 4.0, color: theme.chat.serviceMessage.serviceMessageFillColor)
|
||||
})
|
||||
}
|
||||
|
||||
static func chatInstantVideoBackgroundImage(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? {
|
||||
let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatInstantVideoWithoutWallpaperBackgroundImage : PresentationResourceKey.chatInstantVideoWithWallpaperBackgroundImage
|
||||
return theme.image(key.rawValue, { theme in
|
||||
@ -286,15 +271,9 @@ struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
static func chatEmptyItemBackgroundImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatEmptyItemBackgroundImage.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(radius: 14.0, color: theme.chat.serviceMessage.serviceMessageFillColor)
|
||||
})
|
||||
}
|
||||
|
||||
static func chatEmptyItemIconImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatEmptyItemIconImage.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Chat"), color: theme.chat.serviceMessage.serviceMessagePrimaryTextColor)
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Chat"), color: theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText)
|
||||
})
|
||||
}
|
||||
|
||||
@ -900,12 +879,6 @@ struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
static func chatLoadingIndicatorBackgroundImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatLoadingIndicatorBackgroundImage.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(diameter: 30.0, color: theme.chat.serviceMessage.serviceMessageFillColor)
|
||||
})
|
||||
}
|
||||
|
||||
static func chatBubbleReplyThumbnailPlayImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleReplyThumbnailPlayImage.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
|
||||
@ -1002,8 +975,8 @@ struct PresentationResourcesChat {
|
||||
|
||||
context.translateBy(x: 0.0, y: 1.0)
|
||||
|
||||
context.setFillColor(theme.chat.serviceMessage.serviceMessagePrimaryTextColor.cgColor)
|
||||
context.setStrokeColor(theme.chat.serviceMessage.serviceMessagePrimaryTextColor.cgColor)
|
||||
context.setFillColor(theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText.cgColor)
|
||||
context.setStrokeColor(theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText.cgColor)
|
||||
context.setLineWidth(1.32)
|
||||
|
||||
let _ = try? drawSvgPath(context, path: "M4.5,0.600000024 C5.88071187,0.600000024 7,1.88484952 7,3.46979169 L7,7.39687502 C7,8.9818172 5.88071187,10.2666667 4.5,10.2666667 C3.11928813,10.2666667 2,8.9818172 2,7.39687502 L2,3.46979169 C2,1.88484952 3.11928813,0.600000024 4.5,0.600000024 S ")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
|
||||
public enum PresentationThemeParsingError: Error {
|
||||
case generic
|
||||
@ -563,28 +564,59 @@ public final class PresentationThemeChatBubble {
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationThemeServiceMessage {
|
||||
public let serviceMessageFillColor: UIColor
|
||||
public let serviceMessagePrimaryTextColor: UIColor
|
||||
public let serviceMessageLinkHighlightColor: UIColor
|
||||
public final class PresentationThemeServiceMessageColorComponents {
|
||||
public let fill: UIColor
|
||||
public let primaryText: UIColor
|
||||
public let linkHighlight: UIColor
|
||||
|
||||
public let dateFillStatic: UIColor
|
||||
public let dateFillFloating: UIColor
|
||||
|
||||
public init(fill: UIColor, primaryText: UIColor, linkHighlight: UIColor, dateFillStatic: UIColor, dateFillFloating: UIColor) {
|
||||
self.fill = fill
|
||||
self.primaryText = primaryText
|
||||
self.linkHighlight = linkHighlight
|
||||
self.dateFillStatic = dateFillStatic
|
||||
self.dateFillFloating = dateFillFloating
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationThemeServiceMessageColor {
|
||||
public let withDefaultWallpaper: PresentationThemeServiceMessageColorComponents
|
||||
public let withCustomWallpaper: PresentationThemeServiceMessageColorComponents
|
||||
|
||||
public init(withDefaultWallpaper: PresentationThemeServiceMessageColorComponents, withCustomWallpaper: PresentationThemeServiceMessageColorComponents) {
|
||||
self.withDefaultWallpaper = withDefaultWallpaper
|
||||
self.withCustomWallpaper = withCustomWallpaper
|
||||
}
|
||||
}
|
||||
|
||||
public func serviceMessageColorComponents(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents {
|
||||
return serviceMessageColorComponents(chatTheme: theme.chat, wallpaper: wallpaper)
|
||||
}
|
||||
|
||||
public func serviceMessageColorComponents(chatTheme: PresentationThemeChat, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents {
|
||||
if wallpaper != .builtin {
|
||||
return chatTheme.serviceMessage.components.withCustomWallpaper
|
||||
} else {
|
||||
return chatTheme.serviceMessage.components.withDefaultWallpaper
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationThemeServiceMessage {
|
||||
public let components: PresentationThemeServiceMessageColor
|
||||
|
||||
public let unreadBarFillColor: UIColor
|
||||
public let unreadBarStrokeColor: UIColor
|
||||
public let unreadBarTextColor: UIColor
|
||||
|
||||
public let dateFillStaticColor: UIColor
|
||||
public let dateFillFloatingColor: UIColor
|
||||
public let dateTextColor: UIColor
|
||||
|
||||
public init(serviceMessageFillColor: UIColor, serviceMessagePrimaryTextColor: UIColor, serviceMessageLinkHighlightColor: UIColor, unreadBarFillColor: UIColor, unreadBarStrokeColor: UIColor, unreadBarTextColor: UIColor, dateFillStaticColor: UIColor, dateFillFloatingColor: UIColor, dateTextColor: UIColor) {
|
||||
self.serviceMessageFillColor = serviceMessageFillColor
|
||||
self.serviceMessagePrimaryTextColor = serviceMessagePrimaryTextColor
|
||||
self.serviceMessageLinkHighlightColor = serviceMessageLinkHighlightColor
|
||||
public init(components: PresentationThemeServiceMessageColor, unreadBarFillColor: UIColor, unreadBarStrokeColor: UIColor, unreadBarTextColor: UIColor, dateTextColor: UIColor) {
|
||||
self.components = components
|
||||
self.unreadBarFillColor = unreadBarFillColor
|
||||
self.unreadBarStrokeColor = unreadBarStrokeColor
|
||||
self.unreadBarTextColor = unreadBarTextColor
|
||||
self.dateFillStaticColor = dateFillStaticColor
|
||||
self.dateFillFloatingColor = dateFillFloatingColor
|
||||
self.dateTextColor = dateTextColor
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +76,11 @@ public final class PrincipalThemeEssentialGraphics {
|
||||
public let checkFreeFullImage: UIImage
|
||||
public let checkFreePartialImage: UIImage
|
||||
|
||||
public let chatServiceBubbleFillImage: UIImage
|
||||
public let chatFreeformContentAdditionalInfoBackgroundImage: UIImage
|
||||
public let chatEmptyItemBackgroundImage: UIImage
|
||||
public let chatLoadingIndicatorBackgroundImage: UIImage
|
||||
|
||||
public let clockBubbleIncomingFrameImage: UIImage
|
||||
public let clockBubbleIncomingMinImage: UIImage
|
||||
public let clockBubbleOutgoingFrameImage: UIImage
|
||||
@ -98,9 +103,9 @@ public final class PrincipalThemeEssentialGraphics {
|
||||
public let radialIndicatorFileIconIncoming: UIImage
|
||||
public let radialIndicatorFileIconOutgoing: UIImage
|
||||
|
||||
init(_ theme: PresentationThemeChat, wallpaper: Bool) {
|
||||
let incoming: PresentationThemeBubbleColorComponents = !wallpaper ? theme.bubble.incoming.withoutWallpaper : theme.bubble.incoming.withWallpaper
|
||||
let outgoing: PresentationThemeBubbleColorComponents = !wallpaper ? theme.bubble.outgoing.withoutWallpaper : theme.bubble.outgoing.withWallpaper
|
||||
init(_ theme: PresentationThemeChat, wallpaper: TelegramWallpaper) {
|
||||
let incoming: PresentationThemeBubbleColorComponents = wallpaper.isEmpty ? theme.bubble.incoming.withoutWallpaper : theme.bubble.incoming.withWallpaper
|
||||
let outgoing: PresentationThemeBubbleColorComponents = wallpaper.isEmpty ? theme.bubble.outgoing.withoutWallpaper : theme.bubble.outgoing.withWallpaper
|
||||
|
||||
self.chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, fillColor: incoming.fill, strokeColor: incoming.stroke, neighbors: .none)
|
||||
self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(incoming: true, fillColor: incoming.highlightedFill, strokeColor: incoming.stroke, neighbors: .none)
|
||||
@ -135,8 +140,18 @@ public final class PrincipalThemeEssentialGraphics {
|
||||
self.checkMediaFullImage = generateCheckImage(partial: false, color: .white)!
|
||||
self.checkMediaPartialImage = generateCheckImage(partial: true, color: .white)!
|
||||
|
||||
self.checkFreeFullImage = generateCheckImage(partial: false, color: theme.serviceMessage.serviceMessagePrimaryTextColor)!
|
||||
self.checkFreePartialImage = generateCheckImage(partial: true, color: theme.serviceMessage.serviceMessagePrimaryTextColor)!
|
||||
let serviceColor = serviceMessageColorComponents(chatTheme: theme, wallpaper: wallpaper)
|
||||
self.checkFreeFullImage = generateCheckImage(partial: false, color: serviceColor.primaryText)!
|
||||
self.checkFreePartialImage = generateCheckImage(partial: true, color: serviceColor.primaryText)!
|
||||
|
||||
self.chatServiceBubbleFillImage = generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(serviceColor.fill.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})!.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8)
|
||||
self.chatFreeformContentAdditionalInfoBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: serviceColor.fill)!
|
||||
self.chatEmptyItemBackgroundImage = generateStretchableFilledCircleImage(radius: 14.0, color: serviceColor.fill)!
|
||||
self.chatLoadingIndicatorBackgroundImage = generateStretchableFilledCircleImage(diameter: 30.0, color: serviceColor.fill)!
|
||||
|
||||
self.clockBubbleIncomingFrameImage = generateClockFrameImage(color: theme.bubble.incomingPendingActivityColor)!
|
||||
self.clockBubbleIncomingMinImage = generateClockMinImage(color: theme.bubble.incomingPendingActivityColor)!
|
||||
@ -146,28 +161,28 @@ public final class PrincipalThemeEssentialGraphics {
|
||||
self.clockMediaFrameImage = generateClockFrameImage(color: .white)!
|
||||
self.clockMediaMinImage = generateClockMinImage(color: .white)!
|
||||
|
||||
self.clockFreeFrameImage = generateClockFrameImage(color: theme.serviceMessage.serviceMessagePrimaryTextColor)!
|
||||
self.clockFreeMinImage = generateClockMinImage(color: theme.serviceMessage.serviceMessagePrimaryTextColor)!
|
||||
self.clockFreeFrameImage = generateClockFrameImage(color: serviceColor.primaryText)!
|
||||
self.clockFreeMinImage = generateClockMinImage(color: serviceColor.primaryText)!
|
||||
|
||||
self.dateAndStatusMediaBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.bubble.mediaDateAndStatusFillColor)!
|
||||
self.dateAndStatusFreeBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.serviceMessage.serviceMessageFillColor)!
|
||||
self.dateAndStatusFreeBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: serviceColor.primaryText)!
|
||||
|
||||
let impressionCountImage = UIImage(bundleImageName: "Chat/Message/ImpressionCount")!
|
||||
self.incomingDateAndStatusImpressionIcon = generateTintedImage(image: impressionCountImage, color: theme.bubble.incomingSecondaryTextColor)!
|
||||
self.outgoingDateAndStatusImpressionIcon = generateTintedImage(image: impressionCountImage, color: theme.bubble.outgoingSecondaryTextColor)!
|
||||
self.mediaImpressionIcon = generateTintedImage(image: impressionCountImage, color: .white)!
|
||||
self.freeImpressionIcon = generateTintedImage(image: impressionCountImage, color: theme.serviceMessage.serviceMessagePrimaryTextColor)!
|
||||
self.freeImpressionIcon = generateTintedImage(image: impressionCountImage, color: serviceColor.primaryText)!
|
||||
|
||||
let chatDateSize: CGFloat = 20.0
|
||||
self.dateStaticBackground = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.serviceMessage.dateFillStaticColor.cgColor)
|
||||
context.setFillColor(serviceColor.dateFillStatic.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2)
|
||||
|
||||
self.dateFloatingBackground = generateImage(CGSize(width: chatDateSize, height: chatDateSize), contextGenerator: { size, context -> Void in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(theme.serviceMessage.dateFillFloatingColor.cgColor)
|
||||
context.setFillColor(serviceColor.dateFillFloating.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
})!.stretchableImage(withLeftCapWidth: Int(chatDateSize) / 2, topCapHeight: Int(chatDateSize) / 2)
|
||||
|
||||
|
||||
Binary file not shown.
@ -720,7 +720,8 @@ public func settingsController(account: Account, accountManager: AccountManager)
|
||||
let icon = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")
|
||||
|
||||
let controller = ItemListController(account: account, state: signal, tabBarItem: combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, notificationAuthorizationStatus.get()) |> map { presentationData, status in
|
||||
return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: icon, selectedImage: icon, badgeValue: status != .allowed ? "!" : nil)
|
||||
return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: icon, selectedImage: icon, badgeValue: nil)
|
||||
//return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: icon, selectedImage: icon, badgeValue: status != .allowed ? "!" : nil)
|
||||
})
|
||||
pushControllerImpl = { [weak controller] value in
|
||||
(controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true)
|
||||
|
||||
@ -71,7 +71,6 @@ final class SetupTwoStepVerificationContentNode: ASDisplayNode, UITextFieldDeleg
|
||||
self.inputNode.textField.textContentType = .oneTimeCode
|
||||
}
|
||||
case .email:
|
||||
|
||||
self.inputNode.textField.autocapitalizationType = .none
|
||||
self.inputNode.textField.autocorrectionType = .no
|
||||
self.inputNode.textField.keyboardType = .emailAddress
|
||||
|
||||
@ -268,7 +268,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode {
|
||||
insets.bottom += inputHeight
|
||||
}
|
||||
}
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height))
|
||||
let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: state.layout.layout.size.width, height: state.layout.layout.size.height))
|
||||
if state.data.state?.kind != self.contentNode?.kind {
|
||||
if let dataState = state.data.state {
|
||||
let title: String
|
||||
|
||||
@ -5,7 +5,7 @@ import Display
|
||||
private let textFont: UIFont = Font.regular(16.0)
|
||||
|
||||
final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
private var theme: AuthorizationTheme
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let buttonBackgroundNode: ASImageNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
@ -25,7 +25,7 @@ final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
init(title: String? = nil, theme: AuthorizationTheme, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0) {
|
||||
init(title: String? = nil, theme: PresentationTheme, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0) {
|
||||
self.theme = theme
|
||||
self.buttonHeight = height
|
||||
self.buttonCornerRadius = cornerRadius
|
||||
@ -35,7 +35,7 @@ final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
self.buttonBackgroundNode.isLayerBacked = true
|
||||
self.buttonBackgroundNode.displayWithoutProcessing = true
|
||||
self.buttonBackgroundNode.displaysAsynchronously = false
|
||||
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: cornerRadius, color: theme.accentColor)
|
||||
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: cornerRadius, color: theme.list.itemCheckColors.fillColor)
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
@ -72,7 +72,7 @@ final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
||||
|
||||
if self.title != self.labelNode.attributedText?.string {
|
||||
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.medium(17.0), textColor: self.theme.backgroundColor)
|
||||
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.medium(17.0), textColor: self.theme.list.itemCheckColors.foregroundColor)
|
||||
}
|
||||
|
||||
let labelSize = self.labelNode.updateLayout(buttonSize)
|
||||
|
||||
@ -116,30 +116,4 @@ public final class TelegramRootController: NavigationController {
|
||||
}
|
||||
presentedLegacyShortcutCamera(account: self.account, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller)
|
||||
}
|
||||
|
||||
public func requestPermissions() {
|
||||
guard let parentController = self.viewControllers.last as? ViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
let account = self.account
|
||||
let _ = (DeviceAccess.authorizationStatus(account: account, subject: .notifications)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { status in
|
||||
if status != .allowed {
|
||||
let controller = PermissionController(account: self.account)
|
||||
controller.updateData(subject: .notifications, currentStatus: status, allow: {
|
||||
switch status {
|
||||
case .notDetermined:
|
||||
account.telegramApplicationContext.applicationBindings.registerForNotifications()
|
||||
case .denied:
|
||||
account.telegramApplicationContext.applicationBindings.openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user