Basic implementation of the redesigned profiles

This commit is contained in:
Ali 2020-02-01 00:15:18 +04:00
parent 0a061ebd69
commit 8afd80c31d
12 changed files with 2500 additions and 27 deletions

View File

@ -77,10 +77,18 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut
if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
context.clear(CGRect(origin: CGPoint(), size: displayDimensions))
context.setBlendMode(.copy)
if round && displayDimensions.width != 60.0 {
context.addEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
context.clip()
}
context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
if round {
context.setBlendMode(.destinationOut)
context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
if displayDimensions.width == 60.0 {
context.setBlendMode(.destinationOut)
context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
}
}
} else {
if let emptyColor = emptyColor {

View File

@ -141,6 +141,24 @@ public extension ContainedViewLayoutTransition {
}
}
func updateFrameAdditiveToCenter(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.frame.equalTo(frame) && !force {
completion?(true)
} else {
switch self {
case .immediate:
node.frame = frame
if let completion = completion {
completion(true)
}
case .animated:
let previousFrame = node.frame
node.frame = frame
self.animatePositionAdditive(node: node, offset: CGPoint(x: previousFrame.midX - frame.midX, y: previousFrame.midY - frame.midY))
}
}
}
func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.bounds.equalTo(bounds) && !force {
completion?(true)

View File

@ -125,7 +125,7 @@ public enum GeneralScrollDirection {
}
open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate {
final let scroller: ListViewScroller
public final let scroller: ListViewScroller
private final var visibleSize: CGSize = CGSize()
public private(set) final var insets = UIEdgeInsets()
public final var visualInsets: UIEdgeInsets?

View File

@ -1,29 +1,27 @@
import UIKit
class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
override init(frame: CGRect) {
public final class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
override public init(frame: CGRect) {
super.init(frame: frame)
#if os(iOS)
self.scrollsToTop = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
#endif
}
required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is ListViewTapGestureRecognizer {
return true
}
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers {
for otherGestureRecognizer in gestureRecognizers {
if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 {
@ -36,9 +34,7 @@ class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
}
}
#if os(iOS)
override func touchesShouldCancel(in view: UIView) -> Bool {
override public func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
#endif
}

View File

@ -7,17 +7,3 @@ import TelegramCore
import SyncCore
import AccountContext
public func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? {
if let _ = peer as? TelegramGroup {
return groupInfoController(context: context, peerId: peer.id)
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return groupInfoController(context: context, peerId: peer.id)
} else {
return channelInfoController(context: context, peerId: peer.id)
}
} else if peer is TelegramUser || peer is TelegramSecretChat {
return userInfoController(context: context, peerId: peer.id, mode: mode)
}
return nil
}

View File

@ -0,0 +1,20 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,174 @@
import AsyncDisplayKit
import Display
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let peerId: PeerId
private let listNode: ChatHistoryListNode
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, tagMask: MessageTags) {
self.context = context
self.peerId = peerId
var openMessageImpl: ((MessageId) -> Bool)?
let controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in
return openMessageImpl?(message.id) ?? false
}, openPeer: { _, _, _ in
}, openPeerMention: { _ in
}, openMessageContextMenu: { _, _, _, _, _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in
}, tapMessage: nil, clickThroughMessage: {
}, toggleMessagesSelection: { _, _ in
}, sendCurrentMessage: { _ in
}, sendMessage: { _ in
}, sendSticker: { _, _, _, _ in
return false
}, sendGif: { _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _ in
}, requestMessageActionUrlAuth: { _, _, _ in
}, activateSwitchInline: { _, _ in
}, openUrl: { _, _, _, _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, sendBotCommand: { _, _ in
}, openInstantPage: { _, _ in
}, openWallpaper: { _ in
}, openTheme: {_ in
}, openHashtag: { _, _ in
}, updateInputState: { _ in
}, updateInputMode: { _ in
}, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, navigationController: {
return nil
}, chatControllerNode: {
return nil
}, reactionContainerNode: {
return nil
}, presentGlobalOverlayController: { _, _ in
}, callPeer: { _ in
}, longTap: { _, _ in
}, openCheckoutOrReceipt: { _ in
}, openSearch: {
}, setupReply: { _ in
}, canSetupReply: { _ in
return false
}, navigateToFirstDateMessage: { _ in
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _ in
}, updateMessageReaction: { _, _ in
}, openMessageReactions: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false))
super.init()
openMessageImpl = { id in
return openMessage(id)
}
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
guard let strongSelf = self else {
return
}
var hiddenMedia: [MessageId: [Media]] = [:]
for id in ids {
if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id {
hiddenMedia[messageId] = [media]
}
}
controllerInteraction.hiddenMedia = hiddenMedia
strongSelf.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
itemNode.updateHiddenMedia()
}
}
})
self.listNode.preloadPages = true
self.addSubnode(self.listNode)
self.ready.set(self.listNode.historyState.get()
|> take(1)
|> map { _ -> Bool in true })
}
deinit {
self.hiddenMediaDisposable?.dispose()
}
func scrollToTop() -> Bool {
let offset = self.listNode.visibleContentOffset()
switch offset {
case let .known(value) where value <= CGFloat.ulpOfOne:
return false
default:
self.listNode.scrollToEndOfHistory()
return true
}
}
func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(), duration: duration, curve: curve))
self.listNode.scrollEnabled = !isScrollingLockedAtTop
}
func findLoadedMessage(id: MessageId) -> Message? {
self.listNode.messageInCurrentHistoryView(id)
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
if let result = itemNode.transitionNode(id: messageId, media: media) {
transitionNode = result
}
}
}
return transitionNode
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
enum PeerInfoScreenLabeledValueTextColor {
case primary
case accent
}
enum PeerInfoScreenLabeledValueTextBehavior: Equatable {
case singleLine
case multiLine(maxLines: Int)
}
final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem {
let id: AnyHashable
let label: String
let text: String
let textColor: PeerInfoScreenLabeledValueTextColor
let textBehavior: PeerInfoScreenLabeledValueTextBehavior
let action: (() -> Void)?
init(id: AnyHashable, label: String, text: String, textColor: PeerInfoScreenLabeledValueTextColor = .primary, textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, action: (() -> Void)?) {
self.id = id
self.label = label
self.text = text
self.textColor = textColor
self.textBehavior = textBehavior
self.action = action
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenLabeledValueItemNode()
}
}
private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let labelNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenLabeledValueItem?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.textNode)
}
override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenLabeledValueItem else {
return 10.0
}
self.item = item
self.selectionNode.pressed = item.action
let sideInset: CGFloat = 16.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let textColorValue: UIColor
switch item.textColor {
case .primary:
textColorValue = presentationData.theme.list.itemPrimaryTextColor
case .accent:
textColorValue = presentationData.theme.list.itemAccentColor
}
self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
switch item.textBehavior {
case .singleLine:
self.textNode.maximumNumberOfLines = 1
case let .multiLine(maxLines):
self.textNode.maximumNumberOfLines = maxLines
}
self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue)
let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize)
transition.updateFrame(node: self.labelNode, frame: labelFrame)
transition.updateFrame(node: self.textNode, frame: textFrame)
let height = labelSize.height + 3.0 + textSize.height + 22.0
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
return height
}
}

View File

@ -0,0 +1,55 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let buttonNode: HighlightTrackingButtonNode
let bringToFrontForHighlight: () -> Void
var pressed: (() -> Void)? {
didSet {
self.buttonNode.isUserInteractionEnabled = self.pressed != nil
}
}
init(bringToFrontForHighlight: @escaping () -> Void) {
self.bringToFrontForHighlight = bringToFrontForHighlight
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.bringToFrontForHighlight()
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundNode.alpha = 1.0
} else {
strongSelf.backgroundNode.alpha = 0.0
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
}
}
@objc private func buttonPressed() {
self.pressed?()
}
func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
self.backgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(), size: size))
}
}

View File

@ -0,0 +1,458 @@
import AsyncDisplayKit
import Display
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
private final class VisualMediaItemInteraction {
let openMessage: (MessageId) -> Void
var hiddenMedia: [MessageId: [Media]] = [:]
init(openMessage: @escaping (MessageId) -> Void) {
self.openMessage = openMessage
}
}
private final class VisualMediaItemNode: ASDisplayNode {
private let context: AccountContext
private let interaction: VisualMediaItemInteraction
private let containerNode: ContextControllerSourceNode
private let imageNode: TransformImageNode
private let fetchStatusDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var resourceStatus: MediaResourceStatus?
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
init(context: AccountContext, interaction: VisualMediaItemInteraction) {
self.context = context
self.interaction = interaction
self.containerNode = ContextControllerSourceNode()
self.imageNode = TransformImageNode()
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
self.containerNode.isGestureEnabled = false
}
deinit {
self.fetchStatusDisposable.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let (item, _, _, _) = self.item {
self.interaction.openMessage(item.message.id)
}
}
}
func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) {
if item === self.item?.0 && size == self.item?.2 {
return
}
var media: Media?
for value in item.message.media {
if let image = value as? TelegramMediaImage {
media = image
break
} else if let file = value as? TelegramMediaFile {
media = file
break
}
}
if let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) {
var mediaDimensions: CGSize?
if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions {
mediaDimensions = largestSize.cgSize
self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true)
self.fetchStatusDisposable.set(nil)
/*self.statusNode.transitionToState(.none, completion: { [weak self] in
self?.statusNode.isHidden = true
})*/
//self.mediaBadgeNode.isHidden = true
self.resourceStatus = nil
} else if let file = media as? TelegramMediaFile, file.isVideo {
mediaDimensions = file.dimensions?.cgSize
self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad)
/*self.mediaBadgeNode.isHidden = false
self.resourceStatus = nil
self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: messageId, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, let item = strongSelf.item {
strongSelf.resourceStatus = status
let isStreamable = isMediaStreamable(message: item.message, media: file)
let statusState: RadialStatusNodeState
if isStreamable {
statusState = .none
} else {
switch status {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
case .Local:
statusState = .none
case .Remote:
statusState = .download(.white)
}
}
switch statusState {
case .none:
break
default:
strongSelf.statusNode.isHidden = false
}
strongSelf.statusNode.transitionToState(statusState, animated: true, completion: {
if let strongSelf = self {
if case .none = statusState {
strongSelf.statusNode.isHidden = true
}
}
})
if let duration = file.duration {
let durationString = stringForDuration(duration)
var badgeContent: ChatMessageInteractiveMediaBadgeContent?
var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
if isStreamable {
switch status {
case let .Fetching(_, progress):
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString))
mediaDownloadState = .compactFetching(progress: 0.0)
case .Local:
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
case .Remote:
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
mediaDownloadState = .compactRemote
}
} else {
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
}
strongSelf.mediaBadgeNode.update(theme: item.theme, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)
}
}
}))
if self.statusNode.supernode == nil {
self.imageNode.addSubnode(self.statusNode)
}*/
} else {
//self.mediaBadgeNode.isHidden = true
}
self.item = (item, media, size, mediaDimensions)
self.updateHiddenMedia()
}
if let (item, media, _, mediaDimensions) = self.item {
self.item = (item, media, size, mediaDimensions)
let imageFrame = CGRect(origin: CGPoint(), size: size)
self.containerNode.frame = imageFrame
self.imageNode.frame = imageFrame
if let mediaDimensions = mediaDimensions {
let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))()
}
}
}
func transitionNode() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
let imageNode = self.imageNode
return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in
var statusNodeHidden = false
var accessoryHidden = false
if let strongSelf = self {
//statusNodeHidden = strongSelf.statusNode.isHidden
//accessoryHidden = strongSelf.mediaBadgeNode.isHidden
//strongSelf.statusNode.isHidden = true
//strongSelf.mediaBadgeNode.isHidden = true
}
let view = imageNode?.view.snapshotContentTree(unhide: true)
if let strongSelf = self {
//strongSelf.statusNode.isHidden = statusNodeHidden
//strongSelf.mediaBadgeNode.isHidden = accessoryHidden
}
return (view, nil)
})
}
func updateHiddenMedia() {
if let (item, _, _, _) = self.item {
if let _ = self.interaction.hiddenMedia[item.message.id] {
self.isHidden = true
} else {
self.isHidden = false
}
} else {
self.isHidden = false
}
}
}
private final class VisualMediaItem {
let message: Message
init(message: Message) {
self.message = message
}
}
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
private let context: AccountContext
private let peerId: PeerId
private let scrollNode: ASScrollNode
private var _itemInteraction: VisualMediaItemInteraction?
private var itemInteraction: VisualMediaItemInteraction {
return self._itemInteraction!
}
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
private let listDisposable = MetaDisposable()
private var hiddenMediaDisposable: Disposable?
private var mediaItems: [VisualMediaItem] = []
private var visibleMediaItems: [UInt32: VisualMediaItemNode] = [:]
private var numberOfItemsToRequest: Int = 50
private var currentView: MessageHistoryView?
private var isRequestingView: Bool = false
private var isFirstHistoryView: Bool = true
init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId) {
self.context = context
self.peerId = peerId
self.scrollNode = ASScrollNode()
super.init()
self._itemInteraction = VisualMediaItemInteraction(openMessage: { id in
openMessage(id)
})
self.scrollNode.view.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delegate = self
self.addSubnode(self.scrollNode)
self.requestHistoryAroundVisiblePosition()
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
guard let strongSelf = self else {
return
}
var hiddenMedia: [MessageId: [Media]] = [:]
for id in ids {
if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id {
hiddenMedia[messageId] = [media]
}
}
strongSelf.itemInteraction.hiddenMedia = hiddenMedia
for (_, itemNode) in strongSelf.visibleMediaItems {
itemNode.updateHiddenMedia()
}
})
}
deinit {
self.listDisposable.dispose()
self.hiddenMediaDisposable?.dispose()
}
private func requestHistoryAroundVisiblePosition() {
if self.isRequestingView {
return
}
self.isRequestingView = true
self.listDisposable.set((self.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(self.peerId), index: .upperBound, anchorIndex: .upperBound, count: self.numberOfItemsToRequest, fixedCombinedReadStates: nil, tagMask: .photoOrVideo)
|> deliverOnMainQueue).start(next: { [weak self] (view, updateType, _) in
guard let strongSelf = self else {
return
}
strongSelf.updateHistory(view: view, updateType: updateType)
strongSelf.isRequestingView = false
}))
}
private func updateHistory(view: MessageHistoryView, updateType: ViewUpdateType) {
self.currentView = view
self.mediaItems.removeAll()
switch updateType {
case .FillHole:
self.requestHistoryAroundVisiblePosition()
default:
for entry in view.entries.reversed() {
self.mediaItems.append(VisualMediaItem(message: entry.message))
}
let wasFirstHistoryView = self.isFirstHistoryView
self.isFirstHistoryView = false
if let (size, isScrollingLockedAtTop, presentationData) = self.currentParams {
self.update(size: size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
}
}
}
}
func scrollToTop() -> Bool {
if self.scrollNode.view.contentOffset.y > 0.0 {
self.scrollNode.view.setContentOffset(CGPoint(), animated: true)
return true
} else {
return false
}
}
func findLoadedMessage(id: MessageId) -> Message? {
for item in self.mediaItems {
if item.message.id == id {
return item.message
}
}
return nil
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
for item in self.mediaItems {
if item.message.id == messageId {
if let itemNode = self.visibleMediaItems[item.message.stableId] {
return itemNode.transitionNode()
}
break
}
}
return nil
}
func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
let itemSpacing: CGFloat = 1.0
let itemsInRow: Int = max(3, min(6, Int(size.width / 100.0)))
let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow))
let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1)
let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
self.updateVisibleItems(size: size, theme: presentationData.theme, synchronousLoad: synchronous)
if isScrollingLockedAtTop {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size))
}
self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let (size, _, presentationData) = self.currentParams {
self.updateVisibleItems(size: size, theme: presentationData.theme, synchronousLoad: false)
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil {
if !self.isRequestingView {
self.numberOfItemsToRequest += 50
self.requestHistoryAroundVisiblePosition()
}
}
}
}
private func updateVisibleItems(size: CGSize, theme: PresentationTheme, synchronousLoad: Bool) {
let itemSpacing: CGFloat = 1.0
let itemsInRow: Int = max(3, min(6, Int(size.width / 100.0)))
let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow))
let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1)
let visibleRect = self.scrollNode.view.bounds
var minVisibleRow = Int(floor((visibleRect.minY - itemSpacing) / (itemSize + itemSpacing)))
minVisibleRow = max(0, minVisibleRow)
var maxVisibleRow = Int(ceil((visibleRect.maxY - itemSpacing) / (itemSize + itemSpacing)))
maxVisibleRow = min(rowCount - 1, maxVisibleRow)
let minVisibleIndex = minVisibleRow * itemsInRow
let maxVisibleIndex = min(self.mediaItems.count - 1, maxVisibleRow * itemsInRow - 1)
var validIds = Set<UInt32>()
if minVisibleIndex < maxVisibleIndex {
for i in minVisibleIndex ... maxVisibleIndex {
let stableId = self.mediaItems[i].message.stableId
validIds.insert(stableId)
let rowIndex = i / Int(itemsInRow)
let columnIndex = i % Int(itemsInRow)
let itemOrigin = CGPoint(x: CGFloat(columnIndex) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (size.width - itemOrigin.x) : itemSize, height: itemSize))
let itemNode: VisualMediaItemNode
if let current = self.visibleMediaItems[stableId] {
itemNode = current
} else {
itemNode = VisualMediaItemNode(context: self.context, interaction: self.itemInteraction)
self.visibleMediaItems[stableId] = itemNode
self.scrollNode.addSubnode(itemNode)
}
itemNode.frame = itemFrame
itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: synchronousLoad)
}
}
var removeKeys: [UInt32] = []
for (id, _) in self.visibleMediaItems {
if !validIds.contains(id) {
removeKeys.append(id)
}
}
for id in removeKeys {
if let itemNode = self.visibleMediaItems.removeValue(forKey: id) {
itemNode.removeFromSupernode()
}
}
}
}

View File

@ -1244,3 +1244,20 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
private let defaultChatControllerInteraction = ChatControllerInteraction.default
private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? {
if let _ = peer as? TelegramGroup {
return groupInfoController(context: context, peerId: peer.id)
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
return groupInfoController(context: context, peerId: peer.id)
} else {
return channelInfoController(context: context, peerId: peer.id)
}
} else if peer is TelegramUser {
return PeerInfoScreen(context: context, peerId: peer.id)
} else if peer is TelegramSecretChat {
return userInfoController(context: context, peerId: peer.id, mode: mode)
}
return nil
}