mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Basic implementation of the redesigned profiles
This commit is contained in:
parent
0a061ebd69
commit
8afd80c31d
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
20
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json
vendored
Normal file
20
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
174
submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift
Normal file
174
submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift
Normal 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
|
||||
}
|
||||
}
|
1618
submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift
Normal file
1618
submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
458
submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift
Normal file
458
submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user