Added non-mutual peer flood warning

Added server calls configuration
Added server calls P2P privacy setting
Further Instant View improvements
This commit is contained in:
Ilya Laktyushin 2018-11-06 10:41:40 +04:00
parent 386786d19a
commit 3707aface6
46 changed files with 3070 additions and 2482 deletions

View File

@ -217,38 +217,44 @@ public func blockedPeersController(account: Account) -> ViewController {
controller.peerSelected = { [weak controller] peerId in controller.peerSelected = { [weak controller] peerId in
if let strongController = controller { if let strongController = controller {
strongController.inProgress = true strongController.inProgress = true
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> filter { $0 != nil } let _ = (account.viewTracker.peerView(peerId)
|> take(1) |> take(1)
|> mapToSignal { peers -> Signal<([Peer]?, Peer?), NoError> in |> map { view -> Peer? in
return account.postbox.transaction { transaction -> ([Peer]?, Peer?) in return peerViewMainPeer(view)
return (peers, transaction.getPeer(peerId))
}
} }
|> deliverOnMainQueue |> deliverOnMainQueue).start(next: { peer in
|> mapToSignal { peers, peer -> Signal<Void, NoError> in let applyPeers: Signal<Void, NoError> = peersPromise.get()
if let peers = peers, let peer = peer { |> filter { $0 != nil }
var updatedPeers = peers |> take(1)
for i in 0 ..< updatedPeers.count { |> map { peers -> ([Peer]?, Peer?) in
if updatedPeers[i].id == peerId { return (peers, peer)
updatedPeers.remove(at: i) }
break |> deliverOnMainQueue
|> mapToSignal { peers, peer -> Signal<Void, NoError> in
if let peers = peers, let peer = peer {
var updatedPeers = peers
for i in 0 ..< updatedPeers.count {
if updatedPeers[i].id == peer.id {
updatedPeers.remove(at: i)
break
}
} }
updatedPeers.insert(peer, at: 0)
peersPromise.set(.single(updatedPeers))
} }
updatedPeers.insert(peer, at: 0)
peersPromise.set(.single(updatedPeers)) return .complete()
} }
if let peer = peer {
return .complete() removePeerDisposable.set((requestUpdatePeerIsBlocked(account: account, peerId: peer.id, isBlocked: true) |> then(applyPeers) |> deliverOnMainQueue).start(completed: {
} if let strongController = controller {
removePeerDisposable.set((requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: true) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in strongController.inProgress = false
strongController.dismiss()
}, completed: { }
if let strongController = controller { }))
strongController.inProgress = false
strongController.dismiss()
} }
})) })
} }
} }
presentControllerImpl?(controller, nil) presentControllerImpl?(controller, nil)
@ -272,7 +278,6 @@ public func blockedPeersController(account: Account) -> ViewController {
} }
peersPromise.set(.single(updatedPeers)) peersPromise.set(.single(updatedPeers))
} }
return .complete() return .complete()
} }

View File

@ -426,12 +426,12 @@ final class CallControllerNode: ASDisplayNode {
let point = recognizer.location(in: recognizer.view) let point = recognizer.location(in: recognizer.view)
if self.statusNode.frame.contains(point) { if self.statusNode.frame.contains(point) {
let timestamp = CACurrentMediaTime() let timestamp = CACurrentMediaTime()
if self.debugTapCounter.0 < timestamp - 0.4 { if self.debugTapCounter.0 < timestamp - 0.75 {
self.debugTapCounter.0 = timestamp self.debugTapCounter.0 = timestamp
self.debugTapCounter.1 = 0 self.debugTapCounter.1 = 0
} }
if self.debugTapCounter.0 >= timestamp - 0.4 { if self.debugTapCounter.0 >= timestamp - 0.75 {
self.debugTapCounter.0 = timestamp self.debugTapCounter.0 = timestamp
self.debugTapCounter.1 += 1 self.debugTapCounter.1 += 1
} }
@ -568,6 +568,8 @@ final private class CallDebugNode: ASDisplayNode {
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
private let textNode: ASTextNode private let textNode: ASTextNode
private let timestamp = CACurrentMediaTime()
public var dismiss: (() -> Void)? public var dismiss: (() -> Void)?
init(signal: Signal<(String, String), NoError>) { init(signal: Signal<(String, String), NoError>) {
@ -607,7 +609,9 @@ final private class CallDebugNode: ASDisplayNode {
} }
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
self.dismiss?() if CACurrentMediaTime() - self.timestamp > 1.0 {
self.dismiss?()
}
} }
override func layout() { override func layout() {

View File

@ -109,6 +109,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
private let messageContextDisposable = MetaDisposable() private let messageContextDisposable = MetaDisposable()
private let controllerNavigationDisposable = MetaDisposable() private let controllerNavigationDisposable = MetaDisposable()
private let sentMessageEventsDisposable = MetaDisposable() private let sentMessageEventsDisposable = MetaDisposable()
private let failedMessageEventsDisposable = MetaDisposable()
private let messageActionCallbackDisposable = MetaDisposable() private let messageActionCallbackDisposable = MetaDisposable()
private let editMessageDisposable = MetaDisposable() private let editMessageDisposable = MetaDisposable()
private let enqueueMediaMessageDisposable = MetaDisposable() private let enqueueMediaMessageDisposable = MetaDisposable()
@ -738,50 +739,41 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
if let strongSelf = self { if let strongSelf = self {
strongSelf.commitPurposefulAction() strongSelf.commitPurposefulAction()
func getUserPeer(postbox: Postbox, peerId: PeerId) -> Signal<Peer?, NoError> { let _ = (account.viewTracker.peerView(peerId)
return postbox.transaction { transaction -> Peer? in |> take(1)
guard let peer = transaction.getPeer(peerId) else { |> map { view -> Peer? in
return nil return peerViewMainPeer(view)
} }
if let peer = peer as? TelegramSecretChat { |> deliverOnMainQueue).start(next: { peer in
return transaction.getPeer(peer.regularPeerId) guard let peer = peer else {
return
}
if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData, cachedUserData.callsPrivate {
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: false)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peer.id {
account.telegramApplicationContext.navigateToCurrentCall?()
} else { } else {
return peer let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let _ = (account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(peer.id), transaction.getPeer(currentPeerId))
} |> deliverOnMainQueue).start(next: { peer, current in
if let peer = peer, let current = current {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: true)
})]), in: .window(.root))
}
})
} }
} }
} })
let _ = (getUserPeer(postbox: strongSelf.account.postbox, peerId: peerId)
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData, cachedUserData.callsPrivate {
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
return
}
let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: false)
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
if currentPeerId == peer.id {
account.telegramApplicationContext.navigateToCurrentCall?()
} else {
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let _ = (account.postbox.transaction { transaction -> (Peer?, Peer?) in
return (transaction.getPeer(peer.id), transaction.getPeer(currentPeerId))
} |> deliverOnMainQueue).start(next: { peer, current in
if let peer = peer, let current = current {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peer.id, endCurrentIfAny: true)
})]), in: .window(.root))
}
})
}
}
})
} }
}, longTap: { [weak self] action in }, longTap: { [weak self] action in
if let strongSelf = self { if let strongSelf = self {
@ -1438,6 +1430,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
self.messageContextDisposable.dispose() self.messageContextDisposable.dispose()
self.controllerNavigationDisposable.dispose() self.controllerNavigationDisposable.dispose()
self.sentMessageEventsDisposable.dispose() self.sentMessageEventsDisposable.dispose()
self.failedMessageEventsDisposable.dispose()
self.messageActionCallbackDisposable.dispose() self.messageActionCallbackDisposable.dispose()
self.editMessageDisposable.dispose() self.editMessageDisposable.dispose()
self.enqueueMediaMessageDisposable.dispose() self.enqueueMediaMessageDisposable.dispose()
@ -2049,6 +2042,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
} }
}, forwardSelectedMessages: { [weak self] in }, forwardSelectedMessages: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.commitPurposefulAction()
if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds {
let forwardMessageIds = Array(forwardMessageIdsSet).sorted() let forwardMessageIds = Array(forwardMessageIdsSet).sorted()
strongSelf.forwardMessages(messageIds: forwardMessageIds) strongSelf.forwardMessages(messageIds: forwardMessageIds)
@ -2056,11 +2050,13 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
} }
}, forwardMessages: { [weak self] messages in }, forwardMessages: { [weak self] messages in
if let strongSelf = self, !messages.isEmpty { if let strongSelf = self, !messages.isEmpty {
strongSelf.commitPurposefulAction()
let forwardMessageIds = messages.map { $0.id }.sorted() let forwardMessageIds = messages.map { $0.id }.sorted()
strongSelf.forwardMessages(messageIds: forwardMessageIds) strongSelf.forwardMessages(messageIds: forwardMessageIds)
} }
}, shareSelectedMessages: { [weak self] in }, shareSelectedMessages: { [weak self] in
if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty { if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty {
strongSelf.commitPurposefulAction()
let _ = (strongSelf.account.postbox.transaction { transaction -> [Message] in let _ = (strongSelf.account.postbox.transaction { transaction -> [Message] in
var messages: [Message] = [] var messages: [Message] = []
for id in selectedIds { for id in selectedIds {
@ -2787,6 +2783,18 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
} }
} }
})) }))
self.failedMessageEventsDisposable.set((self.account.pendingMessageManager.failedMessageEvents(peerId: peerId)
|> deliverOnMainQueue).start(next: { [weak self] reason in
if let strongSelf = self {
switch reason {
case .flood:
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: nil, text: strongSelf.presentationData.strings.Conversation_SendMessageErrorFlood, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Generic_ErrorMoreInfo, action: {
self?.openPeerMention("spambot", navigation: .chat(textInputState: nil, messageId: nil))
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}
}
}))
case let .group(groupId): case let .group(groupId):
let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(ApplicationSpecificPreferencesKeys.inAppNotificationSettings)]) let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.group(groupId), .total(ApplicationSpecificPreferencesKeys.inAppNotificationSettings)])
self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in self.chatUnreadCountDisposable = (self.account.postbox.combinedView(keys: [unreadCountsKey]) |> deliverOnMainQueue).start(next: { [weak self] views in
@ -4493,7 +4501,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
} }
} }
private func openPeerMention(_ name: String) { private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) {
let disposable: MetaDisposable let disposable: MetaDisposable
if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable { if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable {
disposable = resolvePeerByNameDisposable disposable = resolvePeerByNameDisposable
@ -4531,7 +4539,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
} }
disposable.set((resolveSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in disposable.set((resolveSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in
if let strongSelf = self { if let strongSelf = self {
strongSelf.openResolved(.peer(peerId, .default)) strongSelf.openResolved(.peer(peerId, navigation))
} }
})) }))
} }
@ -4739,7 +4747,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, UID
guard let buttonView = (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view else { guard let buttonView = (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view else {
return nil return nil
} }
if let peer = self.presentationInterfaceState.renderedPeer?.peer, peer.smallProfileImage != nil { if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil {
let galleryController = AvatarGalleryController(account: self.account, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in let galleryController = AvatarGalleryController(account: self.account, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
}, synchronousLoad: true) }, synchronousLoad: true)
galleryController.setHintWillBePresentedInPreviewingContext(true) galleryController.setHintWillBePresentedInPreviewingContext(true)

View File

@ -58,9 +58,26 @@ private let playImage = generateImage(CGSize(width: 15.0, height: 18.0), rotated
private let titleFont = Font.medium(15.0) private let titleFont = Font.medium(15.0)
private let dateFont = Font.regular(14.0) private let dateFont = Font.regular(14.0)
enum ChatItemGalleryFooterContent { enum ChatItemGalleryFooterContent: Equatable {
case info case info
case playback(paused: Bool, seekable: Bool) case playback(paused: Bool, seekable: Bool)
static func ==(lhs: ChatItemGalleryFooterContent, rhs: ChatItemGalleryFooterContent) -> Bool {
switch lhs {
case .info:
if case .info = rhs {
return true
} else {
return false
}
case let .playback(lhsPaused, lhsSeekable):
if case let .playback(rhsPaused, rhsSeekable) = rhs, lhsPaused == rhsPaused, lhsSeekable == rhsSeekable {
return true
} else {
return false
}
}
}
} }
final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
@ -95,7 +112,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
var content: ChatItemGalleryFooterContent = .info { var content: ChatItemGalleryFooterContent = .info {
didSet { didSet {
//if self.content != oldValue { if self.content != oldValue {
switch self.content { switch self.content {
case .info: case .info:
self.authorNameNode.isHidden = false self.authorNameNode.isHidden = false
@ -111,7 +128,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
self.playbackControlButton.isHidden = false self.playbackControlButton.isHidden = false
self.playbackControlButton.setImage(paused ? playImage : pauseImage, for: []) self.playbackControlButton.setImage(paused ? playImage : pauseImage, for: [])
} }
//} }
} }
} }
@ -332,11 +349,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
if let scrubberView = self.scrubberView, scrubberView.superview == self.view { if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
let sideInset: CGFloat = 8.0 + leftInset let sideInset: CGFloat = 8.0 + leftInset
let topInset: CGFloat = 8.0 let topInset: CGFloat = 8.0
let bottomInset: CGFloat = 8.0 let bottomInset: CGFloat = 2.0
panelHeight += 34.0 + topInset + bottomInset panelHeight += 34.0 + topInset + bottomInset
textFrame.origin.y += 34.0 + topInset + bottomInset
scrubberView.frame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: width - sideInset * 2.0, height: 34.0)) scrubberView.frame = CGRect(origin: CGPoint(x: sideInset, y: topInset + textFrame.maxY), size: CGSize(width: width - sideInset * 2.0, height: 34.0))
} }
self.textNode.frame = textFrame self.textNode.frame = textFrame
@ -364,7 +380,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
} }
override func animateIn(fromHeight: CGFloat, transition: ContainedViewLayoutTransition) { override func animateIn(fromHeight: CGFloat, transition: ContainedViewLayoutTransition) {
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: 0.0, y: self.bounds.size.height - fromHeight)) if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
transition.animatePositionAdditive(layer: scrubberView.layer, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight))
scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight))
self.textNode.alpha = 1.0 self.textNode.alpha = 1.0
self.dateNode.alpha = 1.0 self.dateNode.alpha = 1.0
self.authorNameNode.alpha = 1.0 self.authorNameNode.alpha = 1.0
@ -374,6 +394,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode {
} }
override func animateOut(toHeight: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { override func animateOut(toHeight: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
transition.updateFrame(view: scrubberView, frame: scrubberView.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight))
scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
}
transition.updateFrame(node: self.textNode, frame: self.textNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight)) transition.updateFrame(node: self.textNode, frame: self.textNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight))
self.textNode.alpha = 0.0 self.textNode.alpha = 0.0
self.dateNode.alpha = 0.0 self.dateNode.alpha = 0.0

View File

@ -604,7 +604,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
} }
titleColor = presentationData.theme.theme.chat.bubble.incomingAccentTextColor titleColor = presentationData.theme.theme.chat.bubble.incomingAccentTextColor
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill titleHighlightedColor = bubbleColor.fill
} else { } else {
@ -614,9 +613,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
} }
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleColor = presentationData.theme.theme.chat.bubble.outgoingAccentTextColor titleColor = presentationData.theme.theme.chat.bubble.outgoingAccentTextColor
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill titleHighlightedColor = bubbleColor.fill
} }
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor) let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor)

View File

@ -163,7 +163,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog
strongSelf.currentThumbnailContainerNode = node strongSelf.currentThumbnailContainerNode = node
if let node = node { if let node = node {
strongSelf.insertSubnode(node, aboveSubnode: strongSelf.footerNode) strongSelf.insertSubnode(node, aboveSubnode: strongSelf.footerNode)
if let (navigationHeight, layout) = strongSelf.containerLayout { if let (navigationHeight, layout) = strongSelf.containerLayout, !strongSelf.areControlsHidden {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
node.animateIn(fromLeft: fromLeft) node.animateIn(fromLeft: fromLeft)

View File

@ -5,6 +5,7 @@ import AsyncDisplayKit
final class InstantPageAnchorItem: InstantPageItem { final class InstantPageAnchorItem: InstantPageItem {
let wantsNode: Bool = false let wantsNode: Bool = false
let separatesTiles: Bool = false
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let anchor: String let anchor: String
@ -22,7 +23,7 @@ final class InstantPageAnchorItem: InstantPageItem {
func drawInTile(context: CGContext) { func drawInTile(context: CGContext) {
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return nil return nil
} }

View File

@ -6,27 +6,30 @@ import AsyncDisplayKit
final class InstantPageArticleItem: InstantPageItem { final class InstantPageArticleItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let webPage: TelegramMediaWebpage let webPage: TelegramMediaWebpage
let title: String let contentItems: [InstantPageItem]
let description: String let contentSize: CGSize
let cover: TelegramMediaImage? let cover: TelegramMediaImage?
let url: String let url: String
let webpageId: MediaId let webpageId: MediaId
let rtl: Bool
init(frame: CGRect, webPage: TelegramMediaWebpage, title: String, description: String, cover: TelegramMediaImage?, url: String, webpageId: MediaId) { init(frame: CGRect, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool) {
self.frame = frame self.frame = frame
self.webPage = webPage self.webPage = webPage
self.title = title self.contentItems = contentItems
self.description = description self.contentSize = contentSize
self.cover = cover self.cover = cover
self.url = url self.url = url
self.webpageId = webpageId self.webpageId = webpageId
self.rtl = rtl
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageArticleNode(account: account, webPage: self.webPage, strings: strings, theme: theme, title: self.title, description: self.description, cover: self.cover, url: self.url, webpageId: self.webpageId, openUrl: openUrl) return InstantPageArticleNode(account: account, item: self, webPage: self.webPage, strings: strings, theme: theme, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, rtl: self.rtl, openUrl: openUrl)
} }
func matchesAnchor(_ anchor: String) -> Bool { func matchesAnchor(_ anchor: String) -> Bool {
@ -35,7 +38,7 @@ final class InstantPageArticleItem: InstantPageItem {
func matchesNode(_ node: InstantPageNode) -> Bool { func matchesNode(_ node: InstantPageNode) -> Bool {
if let node = node as? InstantPageArticleNode { if let node = node as? InstantPageArticleNode {
return self.webpageId == node.webpageId return self === node.item
} else { } else {
return false return false
} }
@ -60,3 +63,30 @@ final class InstantPageArticleItem: InstantPageItem {
return [] return []
} }
} }
func layoutArticleItem(theme: InstantPageTheme, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem {
let inset: CGFloat = 17.0
var sideInset = inset
let imageSize = CGSize(width: 65.0, height: 65.0)
if cover != nil {
sideInset += imageSize.width + 10.0
}
var contentItems: [InstantPageItem] = []
let (titleItems, titleSize) = layoutTextItemWithString(title, boundingWidth: boundingWidth - inset - sideInset, offset: CGPoint(x: inset, y: 20.0), maxNumberOfLines: 2)
contentItems.append(contentsOf: titleItems)
let (descriptionItems, descriptionSize) = layoutTextItemWithString(description, boundingWidth: boundingWidth - inset - sideInset, offset: CGPoint(x: inset, y: 20.0 + titleSize.height + 14.0), maxNumberOfLines: 2)
contentItems.append(contentsOf: descriptionItems)
var hasRTL = false
for case let item as InstantPageTextItem in contentItems {
if item.containsRTL {
hasRTL = true
break
}
}
let contentSize = CGSize(width: boundingWidth, height: max(93.0, titleSize.height + descriptionSize.height + 20.0 + 14.0 + 20.0))
return InstantPageArticleItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), webPage: webPage, contentItems: contentItems, contentSize: contentSize, cover: cover, url: url, webpageId: webpageId, rtl: rtl || hasRTL)
}

View File

@ -5,25 +5,16 @@ import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
private func isRtl(string: String) -> Bool {
if string.count == 0 {
return false
}
let code = CFStringTokenizerCopyBestStringLanguage(string as CFString, CFRangeMake(0, string.count)) as String
return Locale.characterDirection(forLanguage: code) == .rightToLeft
}
final class InstantPageArticleNode: ASDisplayNode, InstantPageNode { final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
private let titleNode: ASTextNode let item: InstantPageArticleItem
private let descriptionNode: ASTextNode
private var imageNode: TransformImageNode?
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode private let buttonNode: HighlightableButtonNode
let title: String private let contentTile: InstantPageTile
let pageDescription: String private let contentTileNode: InstantPageTileNode
private var imageNode: TransformImageNode?
let url: String let url: String
let webpageId: MediaId let webpageId: MediaId
let cover: TelegramMediaImage? let cover: TelegramMediaImage?
@ -33,13 +24,12 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
private var fetchedDisposable = MetaDisposable() private var fetchedDisposable = MetaDisposable()
init(account: Account, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, title: String, description: String, cover: TelegramMediaImage?, url: String, webpageId: MediaId, openUrl: @escaping (InstantPageUrlItem) -> Void) { init(account: Account, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, openUrl: @escaping (InstantPageUrlItem) -> Void) {
self.title = title self.item = item
self.pageDescription = description
self.url = url self.url = url
self.webpageId = webpageId self.webpageId = webpageId
self.cover = cover self.cover = cover
self.rtl = isRtl(string: title) || isRtl(string: description) self.rtl = rtl
self.openUrl = openUrl self.openUrl = openUrl
self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode = ASDisplayNode()
@ -48,20 +38,15 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
self.buttonNode = HighlightableButtonNode() self.buttonNode = HighlightableButtonNode()
self.titleNode = ASTextNode() self.contentTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height))
self.titleNode.isLayerBacked = true self.contentTile.items.append(contentsOf: contentItems)
self.titleNode.maximumNumberOfLines = 1 self.contentTileNode = InstantPageTileNode(tile: self.contentTile, backgroundColor: .clear)
self.descriptionNode = ASTextNode()
self.descriptionNode.isLayerBacked = true
self.descriptionNode.maximumNumberOfLines = 2
super.init() super.init()
self.addSubnode(self.highlightedBackgroundNode) self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode) self.addSubnode(self.contentTileNode)
self.addSubnode(self.descriptionNode)
if let image = cover { if let image = cover {
let imageNode = TransformImageNode() let imageNode = TransformImageNode()
@ -109,10 +94,9 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel)) self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
self.contentTileNode.frame = self.bounds
var sideInset: CGFloat = 0.0
if let imageNode = self.imageNode, let image = self.cover, let largest = largestImageRepresentation(image.representations) { if let imageNode = self.imageNode, let image = self.cover, let largest = largestImageRepresentation(image.representations) {
sideInset = imageSize.width + inset
let size = largest.dimensions.aspectFilled(imageSize) let size = largest.dimensions.aspectFilled(imageSize)
let boundingSize = imageSize let boundingSize = imageSize
@ -121,26 +105,19 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
apply() apply()
} }
let titleSize = self.titleNode.measure(CGSize(width: size.width - inset * 2.0 - sideInset, height: size.height)) if let imageNode = self.imageNode {
let descriptionSize = self.descriptionNode.measure(CGSize(width: size.width - inset * 2.0 - sideInset, height: size.height)) if self.rtl {
imageNode.frame = CGRect(origin: CGPoint(x: inset, y: 14.0), size: imageSize)
if self.rtl { } else {
if let imageNode = self.imageNode { imageNode.frame = CGRect(origin: CGPoint(x: size.width - inset - imageSize.width, y: 14.0), size: imageSize)
imageNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
} }
self.titleNode.frame = CGRect(origin: CGPoint(x: size.width - titleSize.width - inset, y: 16.0), size: titleSize)
self.descriptionNode.frame = CGRect(origin: CGPoint(x: size.width - descriptionSize.width - inset, y: self.titleNode.frame.maxY + 6.0), size: descriptionSize)
} else {
if let imageNode = self.imageNode {
imageNode.frame = CGRect(origin: CGPoint(x: size.width - inset - imageSize.width, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
}
self.titleNode.frame = CGRect(origin: CGPoint(x: inset, y: 16.0), size: titleSize)
self.descriptionNode.frame = CGRect(origin: CGPoint(x: inset, y: self.titleNode.frame.maxY + 6.0), size: descriptionSize)
} }
} }
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
} }
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
@ -148,12 +125,9 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
} }
func updateHiddenMedia(media: InstantPageMedia?) { func updateHiddenMedia(media: InstantPageMedia?) {
} }
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: UIFont(name: "Georgia", size: 17.0), textColor: theme.panelPrimaryColor)
self.descriptionNode.attributedText = NSAttributedString(string: self.pageDescription, font: theme.serif ? UIFont(name: "Georgia", size: 15.0) : Font.regular(15.0), textColor: theme.panelSecondaryColor)
self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor
} }
} }

View File

@ -6,6 +6,7 @@ import AsyncDisplayKit
final class InstantPageAudioItem: InstantPageItem { final class InstantPageAudioItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
let medias: [InstantPageMedia] let medias: [InstantPageMedia]
let media: InstantPageMedia let media: InstantPageMedia
@ -18,7 +19,7 @@ final class InstantPageAudioItem: InstantPageItem {
self.medias = [media] self.medias = [media]
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageAudioNode(account: account, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia) return InstantPageAudioNode(account: account, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia)
} }

View File

@ -244,6 +244,9 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
@objc func buttonPressed() { @objc func buttonPressed() {
if let _ = self.playbackState { if let _ = self.playbackState {
self.account.telegramApplicationContext.mediaManager?.playlistControl(.playback(.togglePlayPause), type: self.playlistType) self.account.telegramApplicationContext.mediaManager?.playlistControl(.playback(.togglePlayPause), type: self.playlistType)

View File

@ -176,7 +176,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
itemNode.update(strings: strings, theme: theme) itemNode.update(strings: strings, theme: theme)
} }
self.updateVisibleItems() self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
self.updateNavigationBar() self.updateNavigationBar()
self.recursivelyEnsureDisplaySynchronously(true) self.recursivelyEnsureDisplaySynchronously(true)
@ -184,6 +184,33 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
if let currentLayout = self.currentLayout {
for item in 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
}
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
@ -195,25 +222,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
recognizer.delaysTouchesBegan = false recognizer.delaysTouchesBegan = false
recognizer.tapActionAtPoint = { [weak self] point in recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self { if let strongSelf = self {
if let currentLayout = strongSelf.currentLayout { return strongSelf.tapActionAtPoint(point)
for item in currentLayout.items {
let frame = strongSelf.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 item is InstantPageDetailsItem {
return .fail
}
break
}
}
}
} }
return .waitForSingleTap return .waitForSingleTap
} }
@ -303,7 +312,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.view.contentOffset = contentOffset self.scrollNode.view.contentOffset = contentOffset
} }
if shouldUpdateVisibleItems { if shouldUpdateVisibleItems {
self.updateVisibleItems() self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
} }
} }
@ -327,10 +336,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
var expandedDetails: [Int : Bool] = [:] var expandedDetails: [Int : Bool] = [:]
var itemIndex = -1 var detailsIndex = -1
for item in currentLayout.items { for item in currentLayout.items {
if item.wantsNode { if item.wantsNode {
itemIndex += 1
currentLayoutItemsWithNodes.append(item) currentLayoutItemsWithNodes.append(item)
if let group = item.distanceThresholdGroup() { if let group = item.distanceThresholdGroup() {
let count: Int let count: Int
@ -341,14 +349,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
distanceThresholdGroupCount[Int(group)] = count + 1 distanceThresholdGroupCount[Int(group)] = count + 1
} }
if let detailsItem = item as? InstantPageDetailsItem { if let detailsItem = item as? InstantPageDetailsItem {
expandedDetails[itemIndex] = detailsItem.initiallyExpanded detailsIndex += 1
expandedDetails[detailsIndex] = detailsItem.initiallyExpanded
currentDetailsItems.append(detailsItem)
} }
} }
if let item = item as? InstantPageDetailsItem {
currentDetailsItems.append(item)
}
} }
if self.currentExpandedDetails == nil { if self.currentExpandedDetails == nil {
@ -365,7 +371,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: containerLayout.size.width, height: 2000.0)) self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: containerLayout.size.width, height: 2000.0))
} }
func updateVisibleItems(animated: Bool = false) { func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
guard let theme = self.theme else { guard let theme = self.theme else {
return return
} }
@ -373,8 +379,6 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
var visibleTileIndices = Set<Int>() var visibleTileIndices = Set<Int>()
var visibleItemIndices = Set<Int>() var visibleItemIndices = Set<Int>()
let visibleBounds = self.scrollNode.view.bounds
var topNode: ASDisplayNode? var topNode: ASDisplayNode?
let topTileNode = topNode let topTileNode = topNode
if let scrollSubnodes = self.scrollNode.subnodes { if let scrollSubnodes = self.scrollNode.subnodes {
@ -421,9 +425,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
thresholdedItemFrame.origin.y -= itemThreshold thresholdedItemFrame.origin.y -= itemThreshold
thresholdedItemFrame.size.height += itemThreshold * 2.0 thresholdedItemFrame.size.height += itemThreshold * 2.0
if let expanded = self.currentExpandedDetails?[detailsIndex], !expanded { if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] {
collapseOffset += itemFrame.height - 44.0 let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: 44.0)) collapseOffset += itemFrame.height - height
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height))
} }
if visibleBounds.intersects(thresholdedItemFrame) { if visibleBounds.intersects(thresholdedItemFrame) {
@ -442,7 +447,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
let itemIndex = itemIndex let itemIndex = itemIndex
let embedIndex = embedIndex let embedIndex = embedIndex
let detailsIndex = detailsIndex let detailsIndex = detailsIndex
if let itemNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in if let newNode = item.node(account: self.account, strings: self.strings, theme: theme, openMedia: { [weak self] media in
self?.openMedia(media) self?.openMedia(media)
}, openPeer: { [weak self] peerId in }, openPeer: { [weak self] peerId in
self?.openPeer(peerId) self?.openPeer(peerId)
@ -452,23 +457,39 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self?.updateWebEmbedHeight(embedIndex, height) self?.updateWebEmbedHeight(embedIndex, height)
}, updateDetailsExpanded: { [weak self] expanded in }, updateDetailsExpanded: { [weak self] expanded in
self?.updateDetailsExpanded(detailsIndex, expanded) self?.updateDetailsExpanded(detailsIndex, expanded)
}) { }, currentExpandedDetails: self.currentExpandedDetails) {
itemNode.frame = itemFrame newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
// if case let .animated(duration, _) = transition {
// newNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
// }
if let topNode = topNode { if let topNode = topNode {
self.scrollNode.insertSubnode(itemNode, aboveSubnode: topNode) self.scrollNode.insertSubnode(newNode, aboveSubnode: topNode)
} else { } else {
self.scrollNode.insertSubnode(itemNode, at: 0) self.scrollNode.insertSubnode(newNode, at: 0)
}
topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] in
if let strongSelf = self {
strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: true)
}
}
} }
topNode = itemNode
self.visibleItemsWithNodes[itemIndex] = itemNode
} }
} else { } else {
if (itemNode as! ASDisplayNode).frame != itemFrame { if (itemNode as! ASDisplayNode).frame != itemFrame {
let previousFrame = (itemNode as! ASDisplayNode).frame transition.updateFrame(node: (itemNode as! ASDisplayNode), frame: itemFrame)
(itemNode as! ASDisplayNode).frame = itemFrame itemNode?.updateLayout(size: itemFrame.size, transition: transition)
transition.animateFrame(node: (itemNode as! ASDisplayNode), from: previousFrame)
} }
} }
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
}
} }
} }
@ -478,19 +499,19 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
for tile in self.currentLayoutTiles { for tile in self.currentLayoutTiles {
tileIndex += 1 tileIndex += 1
var tileFrame = tile.frame let tileFrame = effectiveFrameForTile(tile)
if tileIndex > 0 {
tileFrame = tileFrame.offsetBy(dx: 0.0, dy: -collapseOffset)
}
var tileVisibleFrame = tileFrame var tileVisibleFrame = tileFrame
tileVisibleFrame.origin.y -= 400.0 tileVisibleFrame.origin.y -= 400.0
tileVisibleFrame.size.height += 400.0 * 2.0 tileVisibleFrame.size.height += 400.0 * 2.0
if tileVisibleFrame.intersects(visibleBounds) || animated { if tileVisibleFrame.intersects(visibleBounds) {
visibleTileIndices.insert(tileIndex) visibleTileIndices.insert(tileIndex)
if visibleTiles[tileIndex] == nil { if self.visibleTiles[tileIndex] == nil {
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor) let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor)
tileNode.frame = tile.frame tileNode.frame = tileFrame
// if case let .animated(duration, _) = transition {
// tileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
// }
if let topNode = topNode { if let topNode = topNode {
self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode) self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode)
} else { } else {
@ -500,9 +521,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.visibleTiles[tileIndex] = tileNode self.visibleTiles[tileIndex] = tileNode
} else { } else {
if visibleTiles[tileIndex]!.frame != tileFrame { if visibleTiles[tileIndex]!.frame != tileFrame {
let previousFrame = visibleTiles[tileIndex]!.frame transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
visibleTiles[tileIndex]!.frame = tileFrame
transition.animateFrame(node: visibleTiles[tileIndex]!, from: previousFrame)
} }
} }
} }
@ -550,7 +569,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateVisibleItems() self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
self.updateNavigationBar() self.updateNavigationBar()
self.previousContentOffset = self.scrollNode.view.contentOffset self.previousContentOffset = self.scrollNode.view.contentOffset
} }
@ -647,16 +666,16 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
var rects: [CGRect]? var rects: [CGRect]?
if let location = location, let currentLayout = self.currentLayout { if let location = location, let currentLayout = self.currentLayout {
for item in currentLayout.items { for item in currentLayout.items {
if item.frame.contains(location) { let itemFrame = effectiveFrameForItem(item)
let itemNodeFrame = item.frame if itemFrame.contains(location) {
var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -item.frame.minX, dy: -item.frame.minY))
var contentOffset = CGPoint() var contentOffset = CGPoint()
if let item = item as? InstantPageTableItem { if let item = item as? InstantPageTableItem {
contentOffset = tableContentOffset(item: item) contentOffset = tableContentOffset(item: item)
} }
var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY))
for i in 0 ..< itemRects.count { for i in 0 ..< itemRects.count {
itemRects[i] = itemRects[i].offsetBy(dx: itemNodeFrame.minX - contentOffset.x, dy: itemNodeFrame.minY).insetBy(dx: -2.0, dy: -2.0) itemRects[i] = itemRects[i].offsetBy(dx: itemFrame.minX - contentOffset.x, dy: itemFrame.minY).insetBy(dx: -2.0, dy: -2.0)
} }
if !itemRects.isEmpty { if !itemRects.isEmpty {
rects = itemRects rects = itemRects
@ -697,21 +716,44 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return contentOffset return contentOffset
} }
private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
for (_, itemNode) in self.visibleItemsWithNodes {
if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
return CGSize(width: item.frame.width, height: detailsNode.effectiveContentSize.height + item.titleHeight)
}
}
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)
}
private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
let layoutOrigin = item.frame.origin let layoutOrigin = item.frame.origin
var origin = layoutOrigin var origin = layoutOrigin
for item in self.currentDetailsItems { for item in self.currentDetailsItems {
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
if !expanded && layoutOrigin.y >= item.frame.maxY { if layoutOrigin.y >= item.frame.maxY {
let offset = 44.0 - item.frame.height let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
origin.y += offset origin.y += height - item.frame.height
} }
} }
if let item = item as? InstantPageDetailsItem { if let item = item as? InstantPageDetailsItem {
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: expanded ? item.frame.height : 44.0)) let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height))
} else { } else {
return CGRect(origin: origin, size: item.frame.size) return CGRect(origin: origin, size: item.frame.size)
} }
@ -720,17 +762,23 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
if let currentLayout = self.currentLayout { if let currentLayout = self.currentLayout {
for item in currentLayout.items { for item in currentLayout.items {
let frame = self.effectiveFrameForItem(item) let itemFrame = self.effectiveFrameForItem(item)
if frame.contains(location) { if itemFrame.contains(location) {
if let item = item as? InstantPageTextItem, item.selectable { if let item = item as? InstantPageTextItem, item.selectable {
return (item, CGPoint()) return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY))
} else if let item = item as? InstantPageTableItem { } else if let item = item as? InstantPageTableItem {
let contentOffset = tableContentOffset(item: item) let contentOffset = tableContentOffset(item: item)
if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -item.frame.minX + contentOffset.x, dy: -item.frame.minY)) { if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
return (textItem, item.frame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y)) return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y))
} }
} else if let item = item as? InstantPageDetailsItem { } 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))
}
}
}
} }
} }
} }
@ -784,10 +832,10 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
])]) ])])
self.present(actionSheet, nil) self.present(actionSheet, nil)
} else if let (item, parentOffset) = self.textItemAtLocation(location) { } else if let (item, parentOffset) = self.textItemAtLocation(location) {
let textNodeFrame = effectiveFrameForItem(item) let textFrame = item.frame
var itemRects = item.lineRects() var itemRects = item.lineRects()
for i in 0 ..< itemRects.count { for i in 0 ..< itemRects.count {
itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textNodeFrame.minX, dy: parentOffset.y + textNodeFrame.minY).insetBy(dx: -2.0, dy: -2.0) 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()) self.updateTextSelectionRects(itemRects, text: item.plainText())
} }
@ -845,6 +893,19 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> InstantPageAnchorItem? {
for item in items {
if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
return item
} else if let item = item as? InstantPageDetailsItem {
if let anchorItem = findAnchorItem(anchor, items: item.items) {
return anchorItem
}
}
}
return nil
}
private func openUrl(_ url: InstantPageUrlItem) { private func openUrl(_ url: InstantPageUrlItem) {
guard let items = self.currentLayout?.items else { guard let items = self.currentLayout?.items else {
return return
@ -853,11 +914,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
if let webPage = self.webPage, url.webpageId == webPage.id, let anchorRange = url.url.range(of: "#") { if let webPage = self.webPage, url.webpageId == webPage.id, let anchorRange = url.url.range(of: "#") {
let anchor = url.url[anchorRange.upperBound...] let anchor = url.url[anchorRange.upperBound...]
if !anchor.isEmpty { if !anchor.isEmpty {
for item in items { if let anchorItem = findAnchorItem(String(anchor), items: items) {
if let item = item as? InstantPageAnchorItem, item.anchor == anchor { self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: anchorItem.frame.origin.y - self.scrollNode.view.contentInset.top), animated: true)
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: item.frame.origin.y - self.scrollNode.view.contentInset.top), animated: true) return
return
}
} }
} }
} }
@ -914,6 +973,18 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
})) }))
} }
private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] {
var medias: [InstantPageMedia] = []
for item in items {
if let detailsItem = item as? InstantPageDetailsItem {
medias.append(contentsOf: mediasFromItems(detailsItem.items))
} else {
medias.append(contentsOf: item.medias)
}
}
return medias
}
private func openMedia(_ media: InstantPageMedia) { private func openMedia(_ media: InstantPageMedia) {
guard let items = self.currentLayout?.items, let webPage = self.webPage else { guard let items = self.currentLayout?.items, let webPage = self.webPage else {
return return
@ -942,11 +1013,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return return
} }
var medias: [InstantPageMedia] = [] var medias: [InstantPageMedia] = mediasFromItems(items)
for item in items {
medias.append(contentsOf: item.medias)
}
medias = medias.filter { medias = medias.filter {
$0.media is TelegramMediaImage $0.media is TelegramMediaImage
} }
@ -1000,7 +1067,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.updateLayout() strongSelf.updateLayout()
strongSelf.updateVisibleItems() strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds)
} }
})) }))
} }
@ -1011,7 +1078,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
currentExpandedDetails[index] = expanded currentExpandedDetails[index] = expanded
self.currentExpandedDetails = currentExpandedDetails self.currentExpandedDetails = currentExpandedDetails
} }
self.updateVisibleItems(animated: true) self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: true)
} }
private func presentSettings() { private func presentSettings() {

View File

@ -2,22 +2,26 @@ import Foundation
import Postbox import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import Display
final class InstantPageDetailsItem: InstantPageItem { final class InstantPageDetailsItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = true
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let title: NSAttributedString let titleItems: [InstantPageItem]
let titleHeight: CGFloat
let items: [InstantPageItem] let items: [InstantPageItem]
let safeInset: CGFloat let safeInset: CGFloat
let rtl: Bool let rtl: Bool
var initiallyExpanded: Bool let initiallyExpanded: Bool
let index: Int let index: Int
init(frame: CGRect, title: NSAttributedString, items: [InstantPageItem], safeInset: CGFloat, rtl: Bool, initiallyExpanded: Bool, index: Int) { init(frame: CGRect, titleItems: [InstantPageItem], titleHeight: CGFloat, items: [InstantPageItem], safeInset: CGFloat, rtl: Bool, initiallyExpanded: Bool, index: Int) {
self.frame = frame self.frame = frame
self.title = title self.titleItems = titleItems
self.titleHeight = titleHeight
self.items = items self.items = items
self.safeInset = safeInset self.safeInset = safeInset
self.rtl = rtl self.rtl = rtl
@ -25,8 +29,12 @@ final class InstantPageDetailsItem: InstantPageItem {
self.index = index self.index = index
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageDetailsNode(account: account, strings: strings, theme: theme, item: self, updateDetailsExpanded: updateDetailsExpanded) var expanded: Bool?
if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] {
expanded = currentlyExpanded
}
return InstantPageDetailsNode(account: account, strings: strings, theme: theme, item: self, openMedia: openMedia, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded)
} }
func matchesAnchor(_ anchor: String) -> Bool { func matchesAnchor(_ anchor: String) -> Bool {
@ -46,21 +54,48 @@ final class InstantPageDetailsItem: InstantPageItem {
} }
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
if count > 3 { return CGFloat.greatestFiniteMagnitude
return 1000.0
} else {
return CGFloat.greatestFiniteMagnitude
}
} }
func drawInTile(context: CGContext) { func drawInTile(context: CGContext) {
} }
func linkSelectionRects(at point: CGPoint) -> [CGRect] { func linkSelectionRects(at point: CGPoint) -> [CGRect] {
if point.y < self.titleHeight {
for item in self.titleItems {
if item.frame.contains(point) {
let rects = item.linkSelectionRects(at: point.offsetBy(dx: -item.frame.minX, dy: -item.frame.minY))
return rects.map { $0.offsetBy(dx: item.frame.minX, dy: item.frame.minY) }
}
}
} else {
for item in self.items {
if item.frame.contains(point) {
let rects = item.linkSelectionRects(at: point.offsetBy(dx: 0.0, dy: -self.titleHeight))
return rects.map { $0.offsetBy(dx: item.frame.minX, dy: item.frame.minY + self.titleHeight) }
}
}
}
return [] return []
} }
} }
func layoutDetailsItem(theme: InstantPageTheme, title: NSAttributedString, boundingWidth: CGFloat, items: [InstantPageItem], contentSize: CGSize, safeInset: CGFloat, rtl: Bool, initiallyExpanded: Bool, index: Int) -> InstantPageDetailsItem { func layoutDetailsItem(theme: InstantPageTheme, title: NSAttributedString, boundingWidth: CGFloat, items: [InstantPageItem], contentSize: CGSize, safeInset: CGFloat, rtl: Bool, initiallyExpanded: Bool, index: Int) -> InstantPageDetailsItem {
return InstantPageDetailsItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: contentSize.height + 44.0), title: title, items: items, safeInset: safeInset, rtl: rtl, initiallyExpanded: initiallyExpanded, index: index) let detailsInset: CGFloat = 17.0 + safeInset
let titleInset: CGFloat = 22.0
let (titleItems, titleSize) = layoutTextItemWithString(title, boundingWidth: boundingWidth - detailsInset * 2.0 - titleInset, offset: CGPoint(x: detailsInset + titleInset, y: 0.0))
let titleHeight = max(44.0, titleSize.height + 26.0)
var offset: CGFloat?
for var item in titleItems {
var itemOffset = floorToScreenPixels((titleHeight - item.frame.height) / 2.0)
if item is InstantPageTextItem {
offset = itemOffset
} else if let offset = offset {
itemOffset = offset
}
item.frame = item.frame.offsetBy(dx: 0.0, dy: itemOffset)
}
return InstantPageDetailsItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: contentSize.height + titleHeight), titleItems: titleItems, titleHeight: titleHeight, items: items, safeInset: safeInset, rtl: rtl, initiallyExpanded: initiallyExpanded, index: index)
} }

View File

@ -5,7 +5,6 @@ import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
private let detailsHeaderHeight: CGFloat = 44.0
private let detailsInset: CGFloat = 17.0 private let detailsInset: CGFloat = 17.0
private let titleInset: CGFloat = 22.0 private let titleInset: CGFloat = 22.0
@ -14,6 +13,10 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
private let strings: PresentationStrings private let strings: PresentationStrings
private let theme: InstantPageTheme private let theme: InstantPageTheme
private let openMedia: (InstantPageMedia) -> Void
private let openPeer: (PeerId) -> Void
private let openUrl: (InstantPageUrlItem) -> Void
var currentLayoutTiles: [InstantPageTile] = [] var currentLayoutTiles: [InstantPageTile] = []
var currentLayoutItemsWithNodes: [InstantPageItem] = [] var currentLayoutItemsWithNodes: [InstantPageItem] = []
var distanceThresholdGroupCount: [Int: Int] = [:] var distanceThresholdGroupCount: [Int: Int] = [:]
@ -21,14 +24,26 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
var visibleTiles: [Int: InstantPageTileNode] = [:] var visibleTiles: [Int: InstantPageTileNode] = [:]
var visibleItemsWithNodes: [Int: InstantPageNode] = [:] var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = []
var requestLayoutUpdate: (() -> Void)?
var currentLayout: InstantPageLayout var currentLayout: InstantPageLayout
let contentSize: CGSize let contentSize: CGSize
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, items: [InstantPageItem], 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.account = account
self.strings = strings self.strings = strings
self.theme = theme self.theme = theme
self.openMedia = openMedia
self.openPeer = openPeer
self.openUrl = openUrl
self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
self.contentSize = contentSize self.contentSize = contentSize
@ -45,9 +60,13 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width) let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width)
var currentDetailsItems: [InstantPageDetailsItem] = []
var currentLayoutItemsWithViews: [InstantPageItem] = [] var currentLayoutItemsWithViews: [InstantPageItem] = []
var distanceThresholdGroupCount: [Int : Int] = [:] var distanceThresholdGroupCount: [Int : Int] = [:]
var expandedDetails: [Int : Bool] = [:]
var detailsIndex = -1
for item in self.currentLayout.items { for item in self.currentLayout.items {
if item.wantsNode { if item.wantsNode {
currentLayoutItemsWithViews.append(item) currentLayoutItemsWithViews.append(item)
@ -60,21 +79,41 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
} }
distanceThresholdGroupCount[Int(group)] = count + 1 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.currentLayoutTiles = currentLayoutTiles
self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews
self.currentDetailsItems = currentDetailsItems
self.distanceThresholdGroupCount = distanceThresholdGroupCount self.distanceThresholdGroupCount = distanceThresholdGroupCount
} }
func updateVisibleItems() { 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 visibleTileIndices = Set<Int>()
var visibleItemIndices = Set<Int>() var visibleItemIndices = Set<Int>()
let visibleBounds = self.bounds // self.scrollNode.view.bounds self.previousVisibleBounds = visibleBounds
var topNode: ASDisplayNode? var topNode: ASDisplayNode?
let topTileNode = topNode
if let scrollSubnodes = self.subnodes { if let scrollSubnodes = self.subnodes {
for node in scrollSubnodes.reversed() { for node in scrollSubnodes.reversed() {
if let node = node as? InstantPageTileNode { if let node = node as? InstantPageTileNode {
@ -84,32 +123,27 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
} }
} }
var tileIndex = -1 var collapseOffset: CGFloat = 0.0
for tile in self.currentLayoutTiles { let transition: ContainedViewLayoutTransition
tileIndex += 1 if animated {
var tileVisibleFrame = tile.frame transition = .animated(duration: 0.3, curve: .spring)
tileVisibleFrame.origin.y -= 400.0 } else {
tileVisibleFrame.size.height += 400.0 * 2.0 transition = .immediate
if tileVisibleFrame.intersects(visibleBounds) {
visibleTileIndices.insert(tileIndex)
if visibleTiles[tileIndex] == nil {
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: .clear)
tileNode.frame = tile.frame
if let topNode = topNode {
self.insertSubnode(tileNode, aboveSubnode: topNode)
} else {
self.insertSubnode(tileNode, at: 0)
}
topNode = tileNode
self.visibleTiles[tileIndex] = tileNode
}
}
} }
var itemIndex = -1 var itemIndex = -1
var embedIndex = -1
var detailsIndex = -1
for item in self.currentLayoutItemsWithNodes { for item in self.currentLayoutItemsWithNodes {
itemIndex += 1 itemIndex += 1
if item is InstantPageWebEmbedItem {
embedIndex += 1
}
if item is InstantPageDetailsItem {
detailsIndex += 1
}
var itemThreshold: CGFloat = 0.0 var itemThreshold: CGFloat = 0.0
if let group = item.distanceThresholdGroup() { if let group = item.distanceThresholdGroup() {
var count: Int = 0 var count: Int = 0
@ -118,10 +152,19 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
} }
itemThreshold = item.distanceThresholdWithGroupCount(count) itemThreshold = item.distanceThresholdWithGroupCount(count)
} }
var itemFrame = item.frame
itemFrame.origin.y -= itemThreshold var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset)
itemFrame.size.height += itemThreshold * 2.0 var thresholdedItemFrame = itemFrame
if visibleBounds.intersects(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) visibleItemIndices.insert(itemIndex)
var itemNode = self.visibleItemsWithNodes[itemIndex] var itemNode = self.visibleItemsWithNodes[itemIndex]
@ -134,28 +177,82 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
} }
if itemNode == nil { if itemNode == nil {
if let itemNode = item.node(account: self.account, strings: self.strings, theme: self.theme, openMedia: { [weak self] media in let itemIndex = itemIndex
//self?.openMedia(media) let embedIndex = embedIndex
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 }, openPeer: { [weak self] peerId in
//self?.openPeer(peerId) self?.openPeer(peerId)
}, openUrl: { [weak self] url in }, openUrl: { [weak self] url in
//self?.openUrl(url) self?.openUrl(url)
}, updateWebEmbedHeight: { [weak self] height in }, updateWebEmbedHeight: { [weak self] height in
//self?.updateWebEmbedHeight(key, height) //self?.updateWebEmbedHeight(embedIndex, height)
}, updateDetailsExpanded: { _ in }, updateDetailsExpanded: { [weak self] expanded in
}) { self?.updateDetailsExpanded(detailsIndex, expanded)
itemNode.frame = item.frame }, currentExpandedDetails: self.currentExpandedDetails) {
newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
// if case let .animated(duration, _) = transition {
// newNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
// }
if let topNode = topNode { if let topNode = topNode {
self.insertSubnode(itemNode, aboveSubnode: topNode) self.insertSubnode(newNode, aboveSubnode: topNode)
} else { } else {
self.insertSubnode(itemNode, at: 0) self.insertSubnode(newNode, at: 0)
}
topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] in
self?.requestLayoutUpdate?()
}
} }
topNode = itemNode
self.visibleItemsWithNodes[itemIndex] = itemNode
} }
} else { } else {
if (itemNode as! ASDisplayNode).frame != item.frame { if (itemNode as! ASDisplayNode).frame != itemFrame {
(itemNode as! ASDisplayNode).frame = item.frame 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 case let .animated(duration, _) = transition {
// tileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
// }
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)
} }
} }
} }
@ -189,6 +286,152 @@ final class InstantPageDetailsContentNode : ASDisplayNode {
self.visibleItemsWithNodes.removeValue(forKey: index) 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()
// }
// }))
// }
}
private func updateDetailsExpanded(_ index: Int, _ expanded: Bool) {
if var currentExpandedDetails = self.currentExpandedDetails {
currentExpandedDetails[index] = expanded
self.currentExpandedDetails = currentExpandedDetails
}
self.requestLayoutUpdate?()
}
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 tableContentOffset(item: InstantPageTableItem) -> CGPoint {
var contentOffset = CGPoint()
for (_, itemNode) in self.visibleItemsWithNodes {
if let itemNode = itemNode as? InstantPageTableNode, itemNode.item === item {
contentOffset = itemNode.contentOffset
break
}
}
return contentOffset
}
private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
for (_, itemNode) in self.visibleItemsWithNodes {
if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
return CGSize(width: item.frame.width, height: detailsNode.effectiveContentSize.height + item.titleHeight)
}
}
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)
}
private 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? InstantPageTableItem {
let contentOffset = tableContentOffset(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 { final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
@ -204,12 +447,14 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
private let buttonNode: HighlightableButtonNode private let buttonNode: HighlightableButtonNode
private let arrowNode: InstantPageDetailsArrowNode private let arrowNode: InstantPageDetailsArrowNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let contentNode: InstantPageDetailsContentNode let contentNode: InstantPageDetailsContentNode
private let updateExpanded: (Bool) -> Void private let updateExpanded: (Bool) -> Void
var expanded: Bool var expanded: Bool
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, item: InstantPageDetailsItem, updateDetailsExpanded: @escaping (Bool) -> Void) { var requestLayoutUpdate: (() -> Void)?
init(account: Account, strings: PresentationStrings, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) {
self.account = account self.account = account
self.strings = strings self.strings = strings
self.theme = theme self.theme = theme
@ -225,39 +470,31 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
self.buttonNode = HighlightableButtonNode() self.buttonNode = HighlightableButtonNode()
self.titleTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: frame.width, height: detailsHeaderHeight)) self.titleTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: frame.width, height: item.titleHeight))
self.titleTile.items.append(contentsOf: item.titleItems)
self.titleTileNode = InstantPageTileNode(tile: self.titleTile, backgroundColor: .clear) self.titleTileNode = InstantPageTileNode(tile: self.titleTile, backgroundColor: .clear)
let titleItems = layoutTextItemWithString(item.title, boundingWidth: frame.size.width - detailsInset * 2.0 - titleInset, offset: CGPoint(x: detailsInset + titleInset, y: 0.0)).0
var offset: CGFloat?
for var item in titleItems {
var itemOffset = floorToScreenPixels((detailsHeaderHeight - item.frame.height) / 2.0)
if item is InstantPageTextItem {
offset = itemOffset
} else if let offset = offset {
itemOffset = offset
}
item.frame = item.frame.offsetBy(dx: 0.0, dy: itemOffset)
}
self.titleTile.items.append(contentsOf: titleItems)
self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: item.initiallyExpanded) if let expanded = currentlyExpanded {
self.expanded = expanded
} else {
self.expanded = item.initiallyExpanded
}
self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded)
self.separatorNode = ASDisplayNode() 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)) 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.expanded = item.initiallyExpanded
super.init() super.init()
self.clipsToBounds = true self.clipsToBounds = true
self.addSubnode(self.contentNode)
self.addSubnode(self.highlightedBackgroundNode) self.addSubnode(self.highlightedBackgroundNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.addSubnode(self.titleTileNode) self.addSubnode(self.titleTileNode)
self.addSubnode(self.arrowNode) self.addSubnode(self.arrowNode)
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.contentNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
@ -280,6 +517,10 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
} }
} }
self.contentNode.requestLayoutUpdate = { [weak self] in
self?.requestLayoutUpdate?()
}
self.update(strings: strings, theme: theme) self.update(strings: strings, theme: theme)
} }
@ -293,11 +534,10 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
self.updateExpanded(expanded) self.updateExpanded(expanded)
} }
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
let size = layout.size
let inset = detailsInset + self.item.safeInset let inset = detailsInset + self.item.safeInset
let lineSize = CGSize(width: frame.width - inset, height: UIScreenPixel) let lineSize = CGSize(width: self.frame.width - inset, height: UIScreenPixel)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: item.rtl ? 0.0 : inset, y: size.height - lineSize.height), size: lineSize)) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: item.rtl ? 0.0 : inset, y: size.height - lineSize.height), size: lineSize))
} }
@ -308,15 +548,10 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
let inset = detailsInset + self.item.safeInset let inset = detailsInset + self.item.safeInset
self.titleTileNode.frame = self.titleTile.frame self.titleTileNode.frame = self.titleTile.frame
self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: detailsHeaderHeight + UIScreenPixel)) self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.item.titleHeight + UIScreenPixel))
self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: detailsHeaderHeight)) self.buttonNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: self.item.titleHeight))
self.arrowNode.frame = CGRect(x: inset, y: floorToScreenPixels((detailsHeaderHeight - 8.0) / 2.0) + 1.0, width: 13.0, height: 8.0) self.arrowNode.frame = CGRect(x: inset, y: floorToScreenPixels((self.item.titleHeight - 8.0) / 2.0) + 1.0, width: 13.0, height: 8.0)
self.contentNode.frame = CGRect(x: 0.0, y: detailsHeaderHeight, width: size.width, height: self.item.frame.height - detailsHeaderHeight) self.contentNode.frame = CGRect(x: 0.0, y: self.item.titleHeight, width: size.width, height: self.item.frame.height - self.item.titleHeight)
let lineSize = CGSize(width: frame.width - inset, height: UIScreenPixel)
self.separatorNode.frame = CGRect(origin: CGPoint(x: item.rtl ? 0.0 : inset, y: size.height - lineSize.height), size: lineSize)
self.contentNode.updateVisibleItems()
} }
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
@ -324,11 +559,11 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
} }
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
return nil return self.contentNode.transitionNode(media: media)
} }
func updateHiddenMedia(media: InstantPageMedia?) { func updateHiddenMedia(media: InstantPageMedia?) {
self.contentNode.updateHiddenMedia(media: media)
} }
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {
@ -336,6 +571,41 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
self.separatorNode.backgroundColor = theme.controlColor self.separatorNode.backgroundColor = theme.controlColor
self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor self.highlightedBackgroundNode.backgroundColor = theme.panelHighlightedBackgroundColor
} }
func updateVisibleItems(visibleBounds: CGRect, animated: Bool) {
if self.bounds.height > self.item.titleHeight {
self.contentNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY), animated: animated)
}
}
func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
if self.titleTileNode.frame.contains(location) {
for case let item as InstantPageTextItem in self.item.titleItems {
if item.frame.contains(location) {
return (item, self.titleTileNode.frame.origin)
}
}
}
else if let (textItem, parentOffset) = self.contentNode.textItemAtLocation(location.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY)) {
return (textItem, self.contentNode.frame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
}
return nil
}
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
if self.titleTileNode.frame.contains(point) {
if self.item.linkSelectionRects(at: point).isEmpty {
return .fail
}
} else if self.contentNode.frame.contains(point) {
return self.contentNode.tapActionAtPoint(_: point.offsetBy(dx: -self.contentNode.frame.minX, dy: -self.contentNode.frame.minY))
}
return .waitForSingleTap
}
var effectiveContentSize: CGSize {
return self.contentNode.effectiveContentSize
}
} }
private final class InstantPageDetailsArrowNodeParameters: NSObject { private final class InstantPageDetailsArrowNodeParameters: NSObject {

View File

@ -6,6 +6,7 @@ import AsyncDisplayKit
final class InstantPageFeedbackItem: InstantPageItem { final class InstantPageFeedbackItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let webPage: TelegramMediaWebpage let webPage: TelegramMediaWebpage
@ -15,7 +16,7 @@ final class InstantPageFeedbackItem: InstantPageItem {
self.webPage = webPage self.webPage = webPage
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageFeedbackNode(account: account, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl) return InstantPageFeedbackNode(account: account, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl)
} }

View File

@ -81,7 +81,9 @@ final class InstantPageFeedbackNode: ASDisplayNode, InstantPageNode {
} }
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
} }
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
@ -89,7 +91,6 @@ final class InstantPageFeedbackNode: ASDisplayNode, InstantPageNode {
} }
func updateHiddenMedia(media: InstantPageMedia?) { func updateHiddenMedia(media: InstantPageMedia?) {
} }
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {

View File

@ -29,6 +29,7 @@ final class InstantPageImageItem: InstantPageItem {
let fit: Bool let fit: Bool
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute] = [], url: InstantPageUrlItem? = nil, interactive: Bool, roundCorners: Bool, fit: Bool) { init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute] = [], url: InstantPageUrlItem? = nil, interactive: Bool, roundCorners: Bool, fit: Bool) {
self.frame = frame self.frame = frame
@ -41,7 +42,7 @@ final class InstantPageImageItem: InstantPageItem {
self.fit = fit self.fit = fit
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageImageNode(account: account, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, url: self.url, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, openUrl: openUrl) return InstantPageImageNode(account: account, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, url: self.url, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, openUrl: openUrl)
} }

View File

@ -91,6 +91,9 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {
if self.theme.imageEmptyColor != theme.imageEmptyColor { if self.theme.imageEmptyColor != theme.imageEmptyColor {
self.theme = theme self.theme = theme

View File

@ -7,10 +7,11 @@ protocol InstantPageItem {
var frame: CGRect { get set } var frame: CGRect { get set }
var wantsNode: Bool { get } var wantsNode: Bool { get }
var medias: [InstantPageMedia] { get } var medias: [InstantPageMedia] { get }
var separatesTiles: Bool { get }
func matchesAnchor(_ anchor: String) -> Bool func matchesAnchor(_ anchor: String) -> Bool
func drawInTile(context: CGContext) func drawInTile(context: CGContext)
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)?
func matchesNode(_ node: InstantPageNode) -> Bool func matchesNode(_ node: InstantPageNode) -> Bool
func linkSelectionRects(at point: CGPoint) -> [CGRect] func linkSelectionRects(at point: CGPoint) -> [CGRect]

View File

@ -674,25 +674,30 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins
let detailsIndex = detailsIndexCounter let detailsIndex = detailsIndexCounter
detailsIndexCounter += 1 detailsIndexCounter += 1
var subDetailsIndex = 0
var previousBlock: InstantPageBlock? var previousBlock: InstantPageBlock?
for subBlock in blocks { for subBlock in blocks {
let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: subitems, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights) let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToWidthAndHeight: false, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights)
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset, y: contentSize.height + spacing)) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
subitems.append(contentsOf: blockItems) subitems.append(contentsOf: blockItems)
contentSize.height += subLayout.contentSize.height + spacing contentSize.height += subLayout.contentSize.height + spacing
previousBlock = subBlock previousBlock = subBlock
} }
let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil) if !blocks.isEmpty {
contentSize.height += closingSpacing let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil)
contentSize.height += closingSpacing
}
let styleStack = InstantPageTextStyleStack() let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
styleStack.push(.lineSpacingFactor(0.685))
let detailsItem = layoutDetailsItem(theme: theme, title: attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth, items: subitems, contentSize: contentSize, safeInset: safeInset, rtl: rtl, initiallyExpanded: expanded, index: detailsIndex) let detailsItem = layoutDetailsItem(theme: theme, title: attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth, items: subitems, contentSize: contentSize, safeInset: safeInset, rtl: rtl, initiallyExpanded: expanded, index: detailsIndex)
return InstantPageLayout(origin: CGPoint(), contentSize: detailsItem.frame.size, items: [detailsItem]) return InstantPageLayout(origin: CGPoint(), contentSize: detailsItem.frame.size, items: [detailsItem])
case let .relatedArticles(title, articles): case let .relatedArticles(title, articles):
var contentSize = CGSize(width: boundingWidth, height: 0.0) var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
@ -713,7 +718,16 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins
cover = media[coverId] as? TelegramMediaImage cover = media[coverId] as? TelegramMediaImage
} }
let item = InstantPageArticleItem(frame: CGRect(origin: CGPoint(x: 0, y: contentSize.height), size: CGSize(width: boundingWidth, height: 93.0)), webPage: webpage, title: article.title ?? "", description: article.description ?? "", cover: cover, url: article.url, webpageId: article.webpageId) var styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .article, link: false)
let title = attributedStringForRichText(.plain(article.title ?? ""), styleStack: styleStack)
styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .caption, link: false)
let description = attributedStringForRichText(.plain(article.description ?? ""), styleStack: styleStack)
let item = layoutArticleItem(theme: theme, webPage: webpage, title: title, description: description, cover: cover, url: article.url, webpageId: article.webpageId, boundingWidth: boundingWidth, rtl: rtl)
item.frame = item.frame.offsetBy(dx: 0.0, dy: contentSize.height)
contentSize.height += item.frame.height contentSize.height += item.frame.height
items.append(item) items.append(item)

View File

@ -4,7 +4,7 @@ import TelegramCore
func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> CGFloat { func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> CGFloat {
if let upper = upper, let lower = lower { if let upper = upper, let lower = lower {
switch (upper, lower) { switch (upper, lower) {
case (_, .cover), (_, .channelBanner), (.details, .details), (.relatedArticles, nil): case (_, .cover), (_, .channelBanner), (.details, .details), (.relatedArticles, nil), (.anchor, _), (_, .anchor):
return 0.0 return 0.0
case (.divider, _), (_, .divider): case (.divider, _), (_, .divider):
return 25.0 return 25.0
@ -49,7 +49,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) ->
} }
} else if let lower = lower { } else if let lower = lower {
switch lower { switch lower {
case .cover, .channelBanner: case .cover, .channelBanner, .details, .anchor:
return 0.0 return 0.0
default: default:
return 24.0 return 24.0

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import AsyncDisplayKit import AsyncDisplayKit
import Display
protocol InstantPageNode { protocol InstantPageNode {
func updateIsVisible(_ isVisible: Bool) func updateIsVisible(_ isVisible: Bool)
@ -7,4 +8,6 @@ protocol InstantPageNode {
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)?
func updateHiddenMedia(media: InstantPageMedia?) func updateHiddenMedia(media: InstantPageMedia?)
func update(strings: PresentationStrings, theme: InstantPageTheme) func update(strings: PresentationStrings, theme: InstantPageTheme)
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
} }

View File

@ -6,6 +6,7 @@ import AsyncDisplayKit
final class InstantPagePeerReferenceItem: InstantPageItem { final class InstantPagePeerReferenceItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let initialPeer: Peer let initialPeer: Peer
@ -19,7 +20,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem {
self.rtl = rtl self.rtl = rtl
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, rtl: self.rtl, openPeer: openPeer) return InstantPagePeerReferenceNode(account: account, strings: strings, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, rtl: self.rtl, openPeer: openPeer)
} }

View File

@ -171,6 +171,9 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
} }
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
private func applyThemeAndStrings(themeUpdated: Bool) { private func applyThemeAndStrings(themeUpdated: Bool) {
if let peer = self.peer { if let peer = self.peer {
self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.panelPrimaryColor) self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.panelPrimaryColor)

View File

@ -15,6 +15,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem {
let interactive: Bool let interactive: Bool
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool) { init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, interactive: Bool) {
self.frame = frame self.frame = frame
@ -23,7 +24,7 @@ final class InstantPagePlayableVideoItem: InstantPageItem {
self.interactive = interactive self.interactive = interactive
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPagePlayableVideoNode(account: account, webPage: self.webPage, media: self.media, interactive: self.interactive, openMedia: openMedia) return InstantPagePlayableVideoNode(account: account, webPage: self.webPage, media: self.media, interactive: self.interactive, openMedia: openMedia)
} }

View File

@ -56,6 +56,9 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode {
} }
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {
} }

View File

@ -17,6 +17,7 @@ final class InstantPageShapeItem: InstantPageItem {
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let wantsNode: Bool = false let wantsNode: Bool = false
let separatesTiles: Bool = false
init(frame: CGRect, shapeFrame: CGRect, shape: InstantPageShape, color: UIColor) { init(frame: CGRect, shapeFrame: CGRect, shape: InstantPageShape, color: UIColor) {
self.frame = frame self.frame = frame
@ -56,7 +57,7 @@ final class InstantPageShapeItem: InstantPageItem {
return false return false
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return nil return nil
} }

View File

@ -7,6 +7,7 @@ final class InstantPageSlideshowItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let webPage: TelegramMediaWebpage let webPage: TelegramMediaWebpage
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
let medias: [InstantPageMedia] let medias: [InstantPageMedia]
init(frame: CGRect, webPage: TelegramMediaWebpage, medias: [InstantPageMedia]) { init(frame: CGRect, webPage: TelegramMediaWebpage, medias: [InstantPageMedia]) {
@ -15,7 +16,7 @@ final class InstantPageSlideshowItem: InstantPageItem {
self.medias = medias self.medias = medias
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageSlideshowNode(account: account, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia) return InstantPageSlideshowNode(account: account, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia)
} }

View File

@ -426,6 +426,9 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode {
self.pagerNode.internalIsVisible = isVisible self.pagerNode.internalIsVisible = isVisible
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {
} }
} }

View File

@ -100,6 +100,7 @@ final class InstantPageTableItem: InstantPageItem {
let horizontalInset: CGFloat let horizontalInset: CGFloat
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = false
let theme: InstantPageTheme let theme: InstantPageTheme
@ -166,7 +167,7 @@ final class InstantPageTableItem: InstantPageItem {
return false return false
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageTableNode(item: self, account: account, strings: strings, theme: theme) return InstantPageTableNode(item: self, account: account, strings: strings, theme: theme)
} }
@ -227,7 +228,7 @@ final class InstantPageTableContentNode: ASDisplayNode {
for cell in self.item.cells { for cell in self.item.cells {
for item in cell.additionalItems { for item in cell.additionalItems {
if item.wantsNode { if item.wantsNode {
if let node = item.node(account: account, strings: strings, theme: theme, openMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }) { if let node = item.node(account: account, strings: strings, theme: theme, openMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY) node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY)
self.addSubnode(node) self.addSubnode(node)
} }
@ -290,7 +291,9 @@ final class InstantPageTableNode: ASScrollNode, InstantPageNode {
} }
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
} }
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? { func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
@ -298,7 +301,6 @@ final class InstantPageTableNode: ASScrollNode, InstantPageNode {
} }
func updateHiddenMedia(media: InstantPageMedia?) { func updateHiddenMedia(media: InstantPageMedia?) {
} }
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {

View File

@ -57,6 +57,7 @@ final class InstantPageTextItem: InstantPageItem {
var alignment: NSTextAlignment = .natural var alignment: NSTextAlignment = .natural
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let wantsNode: Bool = false let wantsNode: Bool = false
let separatesTiles: Bool = false
var selectable: Bool = true var selectable: Bool = true
var containsRTL: Bool { var containsRTL: Bool {
@ -309,7 +310,7 @@ final class InstantPageTextItem: InstantPageItem {
return false return false
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return nil return nil
} }
@ -427,7 +428,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
} }
} }
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, offset: CGPoint, media: [MediaId: Media] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false) -> ([InstantPageItem], CGSize) { func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, offset: CGPoint, media: [MediaId: Media] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0) -> ([InstantPageItem], CGSize) {
if string.length == 0 { if string.length == 0 {
return ([], CGSize()) return ([], CGSize())
} }
@ -490,10 +491,24 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
} }
} }
if lineCharacterCount > 0 { if lineCharacterCount > 0 {
let line = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastIndex, lineCharacterCount), 100.0) var line = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastIndex, lineCharacterCount), 100.0)
let lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil)) var lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
let lineRange = NSMakeRange(lastIndex, lineCharacterCount) let lineRange = NSMakeRange(lastIndex, lineCharacterCount)
var stop = false
if maxNumberOfLines > 0 && lines.count == maxNumberOfLines - 1 {
let attributes = string.attributes(at: lastIndex + lineCharacterCount - 1, effectiveRange: nil)
if let truncateString = CFAttributedStringCreate(nil, "\u{2026}" as CFString, attributes as CFDictionary) {
let truncateToken = CTLineCreateWithAttributedString(truncateString)
let tokenWidth = CGFloat(CTLineGetTypographicBounds(truncateToken, nil, nil, nil) + 3.0)
if let truncatedLine = CTLineCreateTruncatedLine(line, Double(lineWidth - tokenWidth), .end, truncateToken) {
lineWidth += tokenWidth
line = truncatedLine
}
}
stop = true
}
var strikethroughItems: [InstantPageTextStrikethroughItem] = [] var strikethroughItems: [InstantPageTextStrikethroughItem] = []
var markedItems: [InstantPageTextMarkedItem] = [] var markedItems: [InstantPageTextMarkedItem] = []
@ -563,8 +578,12 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
currentLineOrigin.y += fontLineHeight + fontLineSpacing + extraDescent currentLineOrigin.y += fontLineHeight + fontLineSpacing + extraDescent
lastIndex += lineCharacterCount lastIndex += lineCharacterCount
if stop {
break
}
} else { } else {
break; break
} }
} }

View File

@ -39,6 +39,7 @@ enum InstantPageTextCategoryType {
case paragraph case paragraph
case caption case caption
case table case table
case article
} }
struct InstantPageTextCategories { struct InstantPageTextCategories {
@ -48,6 +49,7 @@ struct InstantPageTextCategories {
let paragraph: InstantPageTextAttributes let paragraph: InstantPageTextAttributes
let caption: InstantPageTextAttributes let caption: InstantPageTextAttributes
let table: InstantPageTextAttributes let table: InstantPageTextAttributes
let article: InstantPageTextAttributes
func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes { func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes {
switch type { switch type {
@ -63,11 +65,13 @@ struct InstantPageTextCategories {
return self.caption.withUnderline(link) return self.caption.withUnderline(link)
case .table: case .table:
return self.table.withUnderline(link) return self.table.withUnderline(link)
case .article:
return self.article.withUnderline(link)
} }
} }
func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextCategories { func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextCategories {
return InstantPageTextCategories(kicker: self.kicker.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), table: self.table.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif)) return InstantPageTextCategories(kicker: self.kicker.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), table: self.table.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), article: self.article.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif))
} }
} }
@ -128,7 +132,8 @@ private let lightTheme = InstantPageTheme(
subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: .black), subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: .black),
paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: .black), paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: .black),
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x79828b)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x79828b)),
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: .black) table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: .black),
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 17.0, lineSpacingFactor: 1.0), color: .black)
), ),
serif: false, serif: false,
codeBlockBackgroundColor: UIColor(rgb: 0xf5f8fc), codeBlockBackgroundColor: UIColor(rgb: 0xf5f8fc),
@ -154,7 +159,8 @@ private let sepiaTheme = InstantPageTheme(
subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)), subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0x4f321d)),
paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)), paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)),
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x927e6b)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x927e6b)),
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)) table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d)),
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x4f321d))
), ),
serif: false, serif: false,
codeBlockBackgroundColor: UIColor(rgb: 0xefe7d6), codeBlockBackgroundColor: UIColor(rgb: 0xefe7d6),
@ -180,7 +186,8 @@ private let grayTheme = InstantPageTheme(
subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)), subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xcecece)),
paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)), paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)),
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xa0a0a0)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xa0a0a0)),
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)) table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece)),
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xcecece))
), ),
serif: false, serif: false,
codeBlockBackgroundColor: UIColor(rgb: 0x555556), codeBlockBackgroundColor: UIColor(rgb: 0x555556),
@ -206,7 +213,8 @@ private let darkTheme = InstantPageTheme(
subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)), subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: UIColor(rgb: 0xb0b0b0)),
paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)), paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)),
caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x6a6a6a)), caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0x6a6a6a)),
table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)) table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0)),
article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 17.0, lineSpacingFactor: 1.0), color: UIColor(rgb: 0xb0b0b0))
), ),
serif: false, serif: false,
codeBlockBackgroundColor: UIColor(rgb: 0x131313), codeBlockBackgroundColor: UIColor(rgb: 0x131313),

View File

@ -14,16 +14,15 @@ final class InstantPageTile {
for item in self.items { for item in self.items {
item.drawInTile(context: context) item.drawInTile(context: context)
} }
// context.setFillColor(UIColor.red.cgColor)
// context.fill(CGRect(x: 0.0, y: self.frame.maxY - 1.0, width: self.frame.width, height: 2.0))
context.translateBy(x: self.frame.minX, y: self.frame.minY) context.translateBy(x: self.frame.minX, y: self.frame.minY)
} }
} }
func instantPageTilesFromLayout(_ layout: InstantPageLayout, boundingWidth: CGFloat) -> [InstantPageTile] { func instantPageTilesFromLayout(_ layout: InstantPageLayout, boundingWidth: CGFloat) -> [InstantPageTile] {
var tileByOrigin: [Int: InstantPageTile] = [:] var tileByOrigin: [Int : InstantPageTile] = [:]
let tileHeight: CGFloat = 256.0 let tileHeight: CGFloat = 256.0
var tileHoles: [CGRect] = []
for item in layout.items { for item in layout.items {
if !item.wantsNode { if !item.wantsNode {
let topTileIndex = max(0, Int(floor(item.frame.minY - 10.0) / tileHeight)) let topTileIndex = max(0, Int(floor(item.frame.minY - 10.0) / tileHeight))
@ -38,10 +37,45 @@ func instantPageTilesFromLayout(_ layout: InstantPageLayout, boundingWidth: CGFl
} }
tile.items.append(item) tile.items.append(item)
} }
} else if item.separatesTiles {
tileHoles.append(item.frame)
} }
} }
return tileByOrigin.values.sorted(by: { lhs, rhs in var finalTiles: [InstantPageTile] = []
var usedTiles = Set<Int>()
for hole in tileHoles {
let topTileIndex = max(0, Int(floor(hole.minY - 10.0) / tileHeight))
let bottomTileIndex = max(topTileIndex, Int(floor(hole.maxY + 10.0) / tileHeight))
for i in topTileIndex ... bottomTileIndex {
if let tile = tileByOrigin[i] {
if tile.frame.minY > hole.minY && tile.frame.minY < hole.maxY {
let delta = hole.maxY - tile.frame.minY
let updatedTile = InstantPageTile(frame: CGRect(origin: tile.frame.origin.offsetBy(dx: 0.0, dy: delta), size: CGSize(width: tile.frame.width, height: tile.frame.height - delta)))
updatedTile.items.append(contentsOf: tile.items)
finalTiles.append(updatedTile)
usedTiles.insert(i)
} else if tile.frame.maxY > hole.minY && tile.frame.minY < hole.minY {
let delta = tile.frame.maxY - hole.minY
let updatedTile = InstantPageTile(frame: CGRect(origin: tile.frame.origin, size: CGSize(width: tile.frame.width, height: tile.frame.height - delta)))
updatedTile.items.append(contentsOf: tile.items)
finalTiles.append(updatedTile)
usedTiles.insert(i)
}
}
}
//let holeTile = InstantPageTile(frame: hole)
//finalTiles.append(holeTile)
}
for (index, tile) in tileByOrigin {
if !usedTiles.contains(index) {
finalTiles.append(tile)
}
}
return finalTiles.sorted(by: { lhs, rhs in
return lhs.frame.minY < rhs.frame.minY return lhs.frame.minY < rhs.frame.minY
}) })
} }

View File

@ -6,6 +6,7 @@ import AsyncDisplayKit
final class InstantPageWebEmbedItem: InstantPageItem { final class InstantPageWebEmbedItem: InstantPageItem {
var frame: CGRect var frame: CGRect
let wantsNode: Bool = true let wantsNode: Bool = true
let separatesTiles: Bool = true
let medias: [InstantPageMedia] = [] let medias: [InstantPageMedia] = []
let url: String? let url: String?
@ -19,7 +20,7 @@ final class InstantPageWebEmbedItem: InstantPageItem {
self.enableScrolling = enableScrolling self.enableScrolling = enableScrolling
} }
func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void) -> (InstantPageNode & ASDisplayNode)? { func node(account: Account, strings: PresentationStrings, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> (InstantPageNode & ASDisplayNode)? {
return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight) return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight)
} }

View File

@ -2,6 +2,7 @@ import Foundation
import TelegramCore import TelegramCore
import WebKit import WebKit
import AsyncDisplayKit import AsyncDisplayKit
import Display
private class WeakInstantPageWebEmbedNodeMessageHandler: NSObject, WKScriptMessageHandler { private class WeakInstantPageWebEmbedNodeMessageHandler: NSObject, WKScriptMessageHandler {
private let f: (WKScriptMessage) -> () private let f: (WKScriptMessage) -> ()
@ -119,6 +120,9 @@ final class InstantPageWebEmbedNode: ASDisplayNode, InstantPageNode {
func updateIsVisible(_ isVisible: Bool) { func updateIsVisible(_ isVisible: Bool) {
} }
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
func update(strings: PresentationStrings, theme: InstantPageTheme) { func update(strings: PresentationStrings, theme: InstantPageTheme) {
} }
} }

View File

@ -181,7 +181,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
let peerRevealOptions: [ItemListRevealOption] let peerRevealOptions: [ItemListRevealOption]
if item.editable && item.enabled { if item.editable && item.enabled {
peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_TerminateSession, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_Terminate, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]
} else { } else {
peerRevealOptions = [] peerRevealOptions = []
} }

View File

@ -125,8 +125,9 @@ final class OngoingCallContext {
private let audioSessionDisposable = MetaDisposable() private let audioSessionDisposable = MetaDisposable()
private var networkTypeDisposable: Disposable? private var networkTypeDisposable: Disposable?
init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) { init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>, serializedData: String?) {
let _ = setupLogs let _ = setupLogs
OngoingCallThreadLocalContext.applyServerConfig(serializedData)
self.internalId = internalId self.internalId = internalId
self.callSessionManager = callSessionManager self.callSessionManager = callSessionManager

View File

@ -50,6 +50,7 @@ typedef NS_ENUM(int32_t, OngoingCallNetworkType) {
@interface OngoingCallThreadLocalContext : NSObject @interface OngoingCallThreadLocalContext : NSObject
+ (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction; + (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction;
+ (void)applyServerConfig:(NSString * _Nullable)data;
@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); @property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState);
@property (nonatomic, copy) void (^ _Nullable callEnded)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile); @property (nonatomic, copy) void (^ _Nullable callEnded)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile);

View File

@ -1,6 +1,7 @@
#import "OngoingCallThreadLocalContext.h" #import "OngoingCallThreadLocalContext.h"
#import "../../libtgvoip/VoIPController.h" #import "../../libtgvoip/VoIPController.h"
#import "../../libtgvoip/VoIPServerConfig.h"
#import "../../libtgvoip/os/darwin/SetupLogging.h" #import "../../libtgvoip/os/darwin/SetupLogging.h"
#import <MtProtoKitDynamic/MtProtoKitDynamic.h> #import <MtProtoKitDynamic/MtProtoKitDynamic.h>
@ -180,6 +181,36 @@ static int callControllerNetworkTypeForType(OngoingCallNetworkType type) {
TGVoipLoggingFunction = loggingFunction; TGVoipLoggingFunction = loggingFunction;
} }
+ (void)applyServerConfig:(NSString *)data {
if (data.length == 0) {
return;
}
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[data dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
if (dict != nil) {
std::vector<std::string> result;
char **values = (char **)malloc(sizeof(char *) * (int)dict.count * 2);
memset(values, 0, (int)dict.count * 2);
__block int index = 0;
[dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, __unused BOOL *stop) {
NSString *valueText = [NSString stringWithFormat:@"%@", value];
const char *keyText = [key UTF8String];
const char *valueTextValue = [valueText UTF8String];
values[index] = (char *)malloc(strlen(keyText) + 1);
values[index][strlen(keyText)] = 0;
memcpy(values[index], keyText, strlen(keyText));
values[index + 1] = (char *)malloc(strlen(valueTextValue) + 1);
values[index + 1][strlen(valueTextValue)] = 0;
memcpy(values[index + 1], valueTextValue, strlen(valueTextValue));
index += 2;
}];
tgvoip::ServerConfig::GetSharedInstance()->Update((const char **)values, index);
for (int i = 0; i < (int)dict.count * 2; i++) {
free(values[i]);
}
free(values);
}
}
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueue> _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType { - (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueue> _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType {
self = [super init]; self = [super init];
if (self != nil) { if (self != nil) {

View File

@ -222,13 +222,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
displayData = value.item.displayData displayData = value.item.displayData
let baseColor: UIColor let baseColor = strongSelf.theme.list.itemSecondaryTextColor
if strongSelf.theme.list.itemPrimaryTextColor.isEqual(strongSelf.theme.list.itemAccentColor) {
baseColor = strongSelf.theme.list.controlSecondaryColor
} else {
baseColor = strongSelf.theme.list.itemPrimaryTextColor
}
if value.order != strongSelf.currentOrder { if value.order != strongSelf.currentOrder {
strongSelf.updateOrder?(value.order) strongSelf.updateOrder?(value.order)
strongSelf.currentOrder = value.order strongSelf.currentOrder = value.order

View File

@ -218,7 +218,7 @@ public final class PresentationCall {
private var droppedCall = false private var droppedCall = false
private var dropCallKitCallTimer: SwiftSignalKit.Timer? private var dropCallKitCallTimer: SwiftSignalKit.Timer?
init(account: Account, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, allowP2P: Bool, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) { init(account: Account, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, serializedData: String?, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal<NetworkType, NoError>) {
self.account = account self.account = account
self.audioSession = audioSession self.audioSession = audioSession
self.callSessionManager = callSessionManager self.callSessionManager = callSessionManager
@ -230,7 +230,7 @@ public final class PresentationCall {
self.isOutgoing = isOutgoing self.isOutgoing = isOutgoing
self.peer = peer self.peer = peer
self.ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: currentNetworkType, updatedNetworkType: updatedNetworkType) self.ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: currentNetworkType, updatedNetworkType: updatedNetworkType, serializedData: serializedData)
var didReceiveAudioOutputs = false var didReceiveAudioOutputs = false
self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId) self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId)

View File

@ -4,19 +4,6 @@ import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import Display import Display
private func p2pAllowed(settings: (VoiceCallSettings, VoipConfiguration)?, isContact: Bool) -> Bool {
var mode: VoiceCallP2PMode? = settings?.0.p2pMode
if mode == nil {
mode = settings?.1.defaultP2PMode
}
switch (mode ?? .contacts, isContact) {
case (.always, _), (.contacts, true):
return true
default:
return false
}
}
private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? { private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? {
let enabled = settings?.enableSystemIntegration ?? true let enabled = settings?.enableSystemIntegration ?? true
return enabled ? integration : nil return enabled ? integration : nil
@ -198,6 +185,25 @@ public final class PresentationCallManager {
let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue let configuration = preferences.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue
if let strongSelf = self { if let strongSelf = self {
strongSelf.callSettings = (callSettings, configuration) strongSelf.callSettings = (callSettings, configuration)
if let legacyP2PMode = callSettings.legacyP2PMode {
_ = updateVoiceCallSettingsSettingsInteractively(postbox: postbox, { settings -> VoiceCallSettings in
var settings = settings
settings.legacyP2PMode = nil
return settings
}).start()
let settings: SelectivePrivacySettings
switch legacyP2PMode {
case .always:
settings = .enableEveryone(disableFor: Set<PeerId>())
case .contacts:
settings = .enableContacts(enableFor: Set<PeerId>(), disableFor: Set<PeerId>())
case .never:
settings = .disableEveryone(enableFor: Set<PeerId>())
}
_ = updateSelectiveAccountPrivacySettings(account: account, type: .voiceCallsP2P, settings: settings).start()
}
} }
}) })
} }
@ -213,7 +219,7 @@ public final class PresentationCallManager {
private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState, Bool)], currentNetworkType: NetworkType, enableCallKit: Bool) { private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState, Bool)], currentNetworkType: NetworkType, enableCallKit: Bool) {
if let firstState = ringingStates.first { if let firstState = ringingStates.first {
if self.currentCall == nil { if self.currentCall == nil {
let call = PresentationCall(account: self.account, audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: enableCallKit ? self.callKitIntegration : nil, getDeviceAccessData: self.getDeviceAccessData, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, allowP2P: p2pAllowed(settings: self.callSettings, isContact: firstState.2), proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType) let call = PresentationCall(account: self.account, audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: enableCallKit ? self.callKitIntegration : nil, serializedData: self.callSettings?.1.serializedData, getDeviceAccessData: self.getDeviceAccessData, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType)
self.currentCall = call self.currentCall = call
self.currentCallPromise.set(.single(call)) self.currentCallPromise.set(.single(call))
self.hasActiveCallsPromise.set(true) self.hasActiveCallsPromise.set(true)
@ -312,7 +318,7 @@ public final class PresentationCallManager {
currentCall.rejectBusy() currentCall.rejectBusy()
} }
let call = PresentationCall(account: strongSelf.account, audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings?.0), getDeviceAccessData: strongSelf.getDeviceAccessData, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, allowP2P: p2pAllowed(settings: strongSelf.callSettings, isContact: isContact), proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: strongSelf.networkType) let call = PresentationCall(account: strongSelf.account, audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings?.0), serializedData: strongSelf.callSettings?.1.serializedData, getDeviceAccessData: strongSelf.getDeviceAccessData, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: strongSelf.networkType)
strongSelf.currentCall = call strongSelf.currentCall = call
strongSelf.currentCallPromise.set(.single(call)) strongSelf.currentCallPromise.set(.single(call))
strongSelf.hasActiveCallsPromise.set(true) strongSelf.hasActiveCallsPromise.set(true)

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case currentSessionInfo(PresentationTheme, String) case currentSessionInfo(PresentationTheme, String)
case pendingSessionsHeader(PresentationTheme, String) case pendingSessionsHeader(PresentationTheme, String)
case pendingSession(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case pendingSession(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
case pendingSessionsInfo(PresentationTheme, String)
case otherSessionsHeader(PresentationTheme, String) case otherSessionsHeader(PresentationTheme, String)
case session(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case session(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
case website(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool) case website(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool)
@ -83,7 +84,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
switch self { switch self {
case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo: case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo:
return RecentSessionsSection.currentSession.rawValue return RecentSessionsSection.currentSession.rawValue
case .pendingSessionsHeader, .pendingSession: case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo:
return RecentSessionsSection.pendingSessions.rawValue return RecentSessionsSection.pendingSessions.rawValue
case .otherSessionsHeader, .session, .website: case .otherSessionsHeader, .session, .website:
return RecentSessionsSection.otherSessions.rawValue return RecentSessionsSection.otherSessions.rawValue
@ -106,8 +107,10 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
return .index(5) return .index(5)
case let .pendingSession(_, _, _, _, session, _, _, _): case let .pendingSession(_, _, _, _, session, _, _, _):
return .session(session.hash) return .session(session.hash)
case .otherSessionsHeader: case .pendingSessionsInfo:
return .index(6) return .index(6)
case .otherSessionsHeader:
return .index(7)
case let .session(_, _, _, _, session, _, _, _): case let .session(_, _, _, _, session, _, _, _):
return .session(session.hash) return .session(session.hash)
case let .website(_, _, _, _, website, _, _, _, _): case let .website(_, _, _, _, website, _, _, _, _):
@ -153,6 +156,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .pendingSessionsInfo(lhsTheme, lhsText):
if case let .pendingSessionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .otherSessionsHeader(lhsTheme, lhsText): case let .otherSessionsHeader(lhsTheme, lhsText):
if case let .otherSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .otherSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -244,14 +253,16 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .pendingSessionsHeader(theme, text): case let .pendingSessionsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .otherSessionsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .pendingSession(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): case let .pendingSession(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed):
return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id) arguments.setSessionIdWithRevealedOptions(previousId, id)
}, removeSession: { id in }, removeSession: { id in
arguments.removeSession(id) arguments.removeSession(id)
}) })
case let .pendingSessionsInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .otherSessionsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .session(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): case let .session(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed):
return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in
arguments.setSessionIdWithRevealedOptions(previousId, id) arguments.setSessionIdWithRevealedOptions(previousId, id)
@ -339,14 +350,14 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
let filteredPendingSessions: [RecentAccountSession] = sessions.filter({ $0.flags.contains(.passwordPending) }) let filteredPendingSessions: [RecentAccountSession] = sessions.filter({ $0.flags.contains(.passwordPending) })
if !filteredPendingSessions.isEmpty { if !filteredPendingSessions.isEmpty {
entries.append(.pendingSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_PasswordPending)) entries.append(.pendingSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_IncompleteAttempts))
for i in 0 ..< filteredPendingSessions.count { for i in 0 ..< filteredPendingSessions.count {
if !existingSessionIds.contains(filteredPendingSessions[i].hash) { if !existingSessionIds.contains(filteredPendingSessions[i].hash) {
existingSessionIds.insert(filteredPendingSessions[i].hash) existingSessionIds.insert(filteredPendingSessions[i].hash)
entries.append(.pendingSession(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredPendingSessions[i], enabled: state.removingSessionId != filteredPendingSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredPendingSessions[i].hash)) entries.append(.pendingSession(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredPendingSessions[i], enabled: state.removingSessionId != filteredPendingSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredPendingSessions[i].hash))
} }
} }
entries.append(.pendingSessionsInfo(presentationData.theme, presentationData.strings.AuthSessions_IncompleteAttemptsInfo))
} }
entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions)) entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions))
@ -425,38 +436,53 @@ public func recentSessionsController(account: Account) -> ViewController {
} }
} }
}, removeSession: { sessionId in }, removeSession: { sessionId in
updateState { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
return $0.withUpdatedRemovingSessionId(sessionId) let controller = ActionSheetController(presentationTheme: presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
} }
controller.setItemGroups([
let applySessions: Signal<Void, NoError> = sessionsPromise.get() ActionSheetItemGroup(items: [
|> filter { $0 != nil } ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateSession, color: .destructive, action: {
|> take(1) dismissAction()
|> deliverOnMainQueue
|> mapToSignal { sessions -> Signal<Void, NoError> in updateState {
if let sessions = sessions { return $0.withUpdatedRemovingSessionId(sessionId)
var updatedSessions = sessions
for i in 0 ..< updatedSessions.count {
if updatedSessions[i].hash == sessionId {
updatedSessions.remove(at: i)
break
}
} }
sessionsPromise.set(.single(updatedSessions))
} let applySessions: Signal<Void, NoError> = sessionsPromise.get()
|> filter { $0 != nil }
return .complete() |> take(1)
} |> deliverOnMainQueue
|> mapToSignal { sessions -> Signal<Void, NoError> in
removeSessionDisposable.set((terminateAccountSession(account: account, hash: sessionId) |> then((applySessions |> mapError { _ in TerminateSessionError.generic })) |> deliverOnMainQueue).start(error: { _ in if let sessions = sessions {
updateState { var updatedSessions = sessions
return $0.withUpdatedRemovingSessionId(nil) for i in 0 ..< updatedSessions.count {
} if updatedSessions[i].hash == sessionId {
}, completed: { updatedSessions.remove(at: i)
updateState { break
return $0.withUpdatedRemovingSessionId(nil) }
} }
})) sessionsPromise.set(.single(updatedSessions))
}
return .complete()
}
removeSessionDisposable.set((terminateAccountSession(account: account, hash: sessionId) |> then((applySessions |> mapError { _ in TerminateSessionError.generic })) |> deliverOnMainQueue).start(error: { _ in
updateState {
return $0.withUpdatedRemovingSessionId(nil)
}
}, completed: {
updateState {
return $0.withUpdatedRemovingSessionId(nil)
}
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, terminateOtherSessions: { }, terminateOtherSessions: {
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationTheme: presentationData.theme) let controller = ActionSheetController(presentationTheme: presentationData.theme)

View File

@ -569,7 +569,7 @@ struct SecureIdDocumentFormState: FormControllerInnerState {
result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate, expiryDateError))) result.append(.entry(SecureIdDocumentFormEntry.expiryDate(document.expiryDate, expiryDateError)))
} }
if (self.selfieRequired || self.requestOptionalData) || self.frontSideRequired || self.backSideRequired { if ((self.selfieRequired || self.requestOptionalData) && identity.document != nil) || self.frontSideRequired || self.backSideRequired {
let type = identity.document?.type let type = identity.document?.type
if let last = result.last, case .spacer = last { if let last = result.last, case .spacer = last {

View File

@ -11,7 +11,7 @@ public enum VoiceCallDataSaving: Int32 {
public struct VoiceCallSettings: PreferencesEntry, Equatable { public struct VoiceCallSettings: PreferencesEntry, Equatable {
public var dataSaving: VoiceCallDataSaving public var dataSaving: VoiceCallDataSaving
public var p2pMode: VoiceCallP2PMode? public var legacyP2PMode: VoiceCallP2PMode?
public var enableSystemIntegration: Bool public var enableSystemIntegration: Bool
public static var defaultSettings: VoiceCallSettings { public static var defaultSettings: VoiceCallSettings {
@ -20,23 +20,23 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable {
init(dataSaving: VoiceCallDataSaving, p2pMode: VoiceCallP2PMode?, enableSystemIntegration: Bool) { init(dataSaving: VoiceCallDataSaving, p2pMode: VoiceCallP2PMode?, enableSystemIntegration: Bool) {
self.dataSaving = dataSaving self.dataSaving = dataSaving
self.p2pMode = p2pMode self.legacyP2PMode = p2pMode
self.enableSystemIntegration = enableSystemIntegration self.enableSystemIntegration = enableSystemIntegration
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))! self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))!
if let value = decoder.decodeOptionalInt32ForKey("p2pMode") { if let value = decoder.decodeOptionalInt32ForKey("p2pMode") {
self.p2pMode = VoiceCallP2PMode(rawValue: value) ?? .contacts self.legacyP2PMode = VoiceCallP2PMode(rawValue: value)
} else { } else {
self.p2pMode = nil self.legacyP2PMode = nil
} }
self.enableSystemIntegration = decoder.decodeInt32ForKey("enableSystemIntegration", orElse: 1) != 0 self.enableSystemIntegration = decoder.decodeInt32ForKey("enableSystemIntegration", orElse: 1) != 0
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.dataSaving.rawValue, forKey: "ds") encoder.encodeInt32(self.dataSaving.rawValue, forKey: "ds")
if let p2pMode = self.p2pMode { if let p2pMode = self.legacyP2PMode {
encoder.encodeInt32(p2pMode.rawValue, forKey: "p2pMode") encoder.encodeInt32(p2pMode.rawValue, forKey: "p2pMode")
} else { } else {
encoder.encodeNil(forKey: "p2pMode") encoder.encodeNil(forKey: "p2pMode")
@ -56,7 +56,7 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable {
if lhs.dataSaving != rhs.dataSaving { if lhs.dataSaving != rhs.dataSaving {
return false return false
} }
if lhs.p2pMode != rhs.p2pMode { if lhs.legacyP2PMode != rhs.legacyP2PMode {
return false return false
} }
if lhs.enableSystemIntegration != rhs.enableSystemIntegration { if lhs.enableSystemIntegration != rhs.enableSystemIntegration {