User Info UI improvements

This commit is contained in:
Ali 2020-02-08 23:07:06 +00:00
parent 93a7caf86d
commit 65b22f6ca5
31 changed files with 3264 additions and 2734 deletions

View File

@ -371,7 +371,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in
var preset = preset
if currentPreset == nil {
preset.id = max(1, settings.filters.map({ $0.id }).max() ?? 1)
preset.id = max(2, settings.filters.map({ $0.id }).max() ?? 2)
}
var settings = settings
settings.filters = settings.filters.filter { $0 != preset && $0 != currentPreset }

View File

@ -193,8 +193,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
}
}, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer, contextAction: peerContextAction.flatMap { peerContextAction in
return { node, gesture in
if let chatPeer = peer.peer.peers[peer.peer.peerId] {
if let chatPeer = peer.peer.peers[peer.peer.peerId], chatPeer.id.namespace != Namespaces.Peer.SecretChat {
peerContextAction(chatPeer, .recentSearch, node, gesture)
} else {
gesture?.cancel()
}
}
})
@ -415,7 +417,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
interaction.peerSelected(peer)
}, contextAction: peerContextAction.flatMap { peerContextAction in
return { node, gesture in
if let chatPeer = chatPeer {
if let chatPeer = chatPeer, chatPeer.id.namespace != Namespaces.Peer.SecretChat {
peerContextAction(chatPeer, .search, node, gesture)
} else {
gesture?.cancel()

View File

@ -567,6 +567,30 @@ public extension ContainedViewLayoutTransition {
}
}
func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
let t = view.layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
if currentScale.isEqual(to: fromScale) {
if let completion = completion {
completion(true)
}
return
}
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
view.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}
func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
let t = node.layer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))

View File

@ -38,6 +38,20 @@ public class ImmediateTextNode: TextNode {
let node = TextNode()
node.cachedLayout = self.cachedLayout
node.frame = self.frame
if let subnodes = self.subnodes {
for subnode in subnodes {
if let subnode = subnode as? ASImageNode {
let copySubnode = ASImageNode()
copySubnode.isLayerBacked = subnode.isLayerBacked
copySubnode.image = subnode.image
copySubnode.displaysAsynchronously = false
copySubnode.displayWithoutProcessing = true
copySubnode.frame = subnode.frame
copySubnode.alpha = subnode.alpha
node.addSubnode(copySubnode)
}
}
}
return node
}

View File

@ -133,6 +133,13 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}*/
}
func hasNonReadyControllers() -> Bool {
if let pending = self.state.pending, !pending.isReady {
return true
}
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
@ -185,6 +192,17 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
if let strongSelf = self {
if let top = strongSelf.state.top {
strongSelf.syncKeyboard(leftEdge: top.value.displayNode.frame.minX, transition: transition)
var updatedStatusBarStyle = strongSelf.statusBarStyle
if let childTransition = strongSelf.state.transition, childTransition.coordinator.progress >= 0.3 {
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
if strongSelf.statusBarStyle != updatedStatusBarStyle {
strongSelf.statusBarStyle = updatedStatusBarStyle
strongSelf.statusBarStyleUpdated?(.animated(duration: 0.3, curve: .easeInOut))
}
}
}
})
@ -337,7 +355,11 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
self.applyLayout(layout: updatedLayout, to: top, isMaster: true, transition: transition)
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
if let childTransition = self.state.transition, childTransition.coordinator.progress >= 0.3 {
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
} else {
updatedStatusBarStyle = .Ignore
}

View File

@ -274,6 +274,18 @@ open class NavigationController: UINavigationController, ContainableController,
return true
}
}
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
if container.hasNonReadyControllers() {
return true
}
case let .split(splitContainer):
if splitContainer.hasNonReadyControllers() {
return true
}
}
}
return false
}
@ -1001,27 +1013,8 @@ open class NavigationController: UINavigationController, ContainableController,
}
public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void) {
let navigateAction: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
if !controller.hasActiveInput {
//strongSelf.view.endEditing(true)
}
/*strongSelf.scheduleAfterLayout({
guard let strongSelf = self else {
return
}*/
strongSelf.pushViewController(controller, animated: animated)
completion()
//})
}
/*if let lastController = self.viewControllers.last as? ViewController, !lastController.attemptNavigation(navigateAction) {
} else {*/
navigateAction()
//}
self.pushViewController(controller, animated: animated)
completion()
}
open override func pushViewController(_ viewController: UIViewController, animated: Bool) {

View File

@ -57,6 +57,16 @@ final class NavigationSplitContainer: ASDisplayNode {
self.view.addSubview(self.detailScrollToTopView)
}
func hasNonReadyControllers() -> Bool {
if self.masterContainer.hasNonReadyControllers() {
return true
}
if self.detailContainer.hasNonReadyControllers() {
return true
}
return false
}
func updateTheme(theme: NavigationControllerTheme) {
self.separator.backgroundColor = theme.navigationBar.separatorColor
}

View File

@ -93,6 +93,8 @@ public enum ViewControllerNavigationPresentation {
}
}
var blocksInteractionUntilReady: Bool = false
public final var isOpaqueWhenInOverlay: Bool = false
public final var blocksBackgroundWhenInOverlay: Bool = false
public final var automaticallyControlPresentationContextLayout: Bool = true

View File

@ -15,6 +15,8 @@ static_library(
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
"$SDKROOT/System/Library/Frameworks/Photos.framework",
],
weak_frameworks = [
"Photos",
],
)

View File

@ -26,6 +26,8 @@ static_library(
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
"$SDKROOT/System/Library/Frameworks/QuickLook.framework",
"$SDKROOT/System/Library/Frameworks/Photos.framework",
],
weak_frameworks = [
"Photos",
],
)

View File

@ -18,6 +18,8 @@ static_library(
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
"$SDKROOT/System/Library/Frameworks/MobileCoreServices.framework",
"$SDKROOT/System/Library/Frameworks/Photos.framework",
],
weak_frameworks = [
"Photos",
],
)

View File

@ -22,7 +22,7 @@ public final class SearchDisplayController {
private var isSearchingDisposable: Disposable?
public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) {
public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) {
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings, fieldStyle: .modern)
self.mode = mode
self.contentNode = contentNode
@ -48,6 +48,9 @@ public final class SearchDisplayController {
self?.searchBar.prefixString = prefix
self?.searchBar.text = query
}
if let placeholder = placeholder {
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
self.contentNode.setPlaceholder = { [weak self] string in
guard string != self?.searchBar.placeholderString?.string else {
return
@ -153,6 +156,7 @@ public final class SearchDisplayController {
if let placeholder = placeholder {
self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} else {
self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
}
}

View File

@ -92,8 +92,10 @@ static_library(
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
"$SDKROOT/System/Library/Frameworks/MessageUI.framework",
"$SDKROOT/System/Library/Frameworks/LocalAuthentication.framework",
"$SDKROOT/System/Library/Frameworks/Photos.framework",
"$SDKROOT/System/Library/Frameworks/QuickLook.framework",
"$SDKROOT/System/Library/Frameworks/CoreTelephony.framework",
],
weak_frameworks = [
"Photos",
],
)

View File

@ -80,12 +80,20 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
(self.view as? ChatAvatarNavigationNodeView)?.targetNode = self
(self.view as? ChatAvatarNavigationNodeView)?.chatController = self.chatController
self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:))))
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))
self.avatarNode.view.addGestureRecognizer(tapRecognizer)
}
@objc private func avatarTapGesture(_ recognizer: UITapGestureRecognizer) {
@objc private func avatarTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state {
self.tapped?()
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
self.tapped?()
default:
break
}
}
}
}

View File

@ -1866,12 +1866,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.controllerInteraction = controllerInteraction
var displayNavigationAvatar = false
if case let .peer(peerId) = chatLocation, peerId != context.account.peerId {
displayNavigationAvatar = true
if case let .peer(peerId) = chatLocation, peerId != context.account.peerId, subject != .scheduledMessages {
self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId)
}
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, displayAvatar: displayNavigationAvatar)
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, displayAvatar: true)
if let avatarNode = self.chatTitleView?.avatarNode {
avatarNode.chatController = self
avatarNode.contextAction = { [weak self] node, gesture in
@ -1988,8 +1986,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
if let peer = peerViewMainPeer(peerView) {
strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages)
strongSelf.chatTitleView?.avatarNode?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: peer.isDeleted ? .deletedIcon : .none)
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil
let imageOverride: AvatarNodeImageOverride?
if strongSelf.context.account.peerId == peer.id {
imageOverride = .savedMessagesIcon
} else if peer.isDeleted {
imageOverride = .deletedIcon
} else {
imageOverride = nil
}
strongSelf.chatTitleView?.avatarNode?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride)
strongSelf.chatTitleView?.avatarNode?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && imageOverride == nil && peer.smallProfileImage != nil
}
if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages {
@ -5075,6 +5081,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.leftNavigationButton = nil
}
self.chatTitleView?.displayAvatar = updatedChatPresentationInterfaceState.interfaceState.selectionState == nil
if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) {
if self.rightNavigationButton != button {
var animated = transition.isAnimated

View File

@ -72,7 +72,12 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch
}
}
if presentationInterfaceState.isScheduledMessages {
return nil
}
if case .standard(true) = presentationInterfaceState.mode {
return nil
} else if let peer = presentationInterfaceState.renderedPeer?.peer {
if presentationInterfaceState.accountPeerId == peer.id {
if presentationInterfaceState.isScheduledMessages {

View File

@ -179,6 +179,15 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var pressed: (() -> Void)?
var displayAvatar: Bool = true {
didSet {
if self.displayAvatar != oldValue {
self.avatarNode?.isHidden = !self.displayAvatar
self.setNeedsLayout()
}
}
}
var titleContent: ChatTitleContent? {
didSet {
if let titleContent = self.titleContent {
@ -529,19 +538,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
if highlighted {
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.activityNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleLeftIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleRightIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleCredibilityIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.4
strongSelf.activityNode.alpha = 0.4
strongSelf.titleLeftIconNode.alpha = 0.4
strongSelf.titleRightIconNode.alpha = 0.4
strongSelf.titleCredibilityIconNode.alpha = 0.4
} else {
strongSelf.titleNode.alpha = 1.0
strongSelf.activityNode.alpha = 1.0
strongSelf.titleLeftIconNode.alpha = 1.0
strongSelf.titleRightIconNode.alpha = 1.0
strongSelf.titleCredibilityIconNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
@ -592,7 +595,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
if let image = self.titleLeftIconNode.image {
if self.titleLeftIconNode.supernode == nil {
self.contentContainer.addSubnode(self.titleLeftIconNode)
self.titleNode.addSubnode(self.titleLeftIconNode)
}
leftIconWidth = image.size.width + 6.0
} else if self.titleLeftIconNode.supernode != nil {
@ -610,7 +613,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
if let image = self.titleRightIconNode.image {
if self.titleRightIconNode.supernode == nil {
self.contentContainer.addSubnode(self.titleRightIconNode)
self.titleNode.addSubnode(self.titleRightIconNode)
}
rightIconWidth = image.size.width + 3.0
} else if self.titleRightIconNode.supernode != nil {
@ -622,68 +625,43 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
let avatarSize = CGSize(width: 37.0, height: 37.0)
let avatarFrame = CGRect(origin: CGPoint(x: leftInset + 10.0, y: floor((size.height - avatarSize.height) / 2.0)), size: avatarSize)
avatarNode.frame = avatarFrame
leftInset += avatarSize.width + 10.0 + 8.0
if self.displayAvatar {
leftInset += avatarSize.width + 10.0 + 8.0
}
}
self.button.frame = CGRect(origin: CGPoint(x: leftInset - 20.0, y: 0.0), size: CGSize(width: clearBounds.width - leftInset, height: size.height))
let titleSideInset: CGFloat = 3.0
if size.height > 40.0 {
var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0 - leftInset, height: size.height))
titleSize.width += credibilityIconWidth
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .left)
let titleInfoSpacing: CGFloat = 0.0
var titleFrame: CGRect
if activitySize.height.isZero {
titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
} else {
let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing
titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
var activityFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize)
self.activityNode.frame = activityFrame
}
if let image = self.titleLeftIconNode.image {
self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX - image.size.width - 3.0 - UIScreenPixel, y: titleFrame.minY + 4.0), size: image.size)
}
if let image = self.titleCredibilityIconNode.image {
self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 2.0), size: image.size)
}
if let image = self.titleRightIconNode.image {
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + 6.0), size: image.size)
}
} else {
let titleSize = self.titleNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height))
let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center)
let titleInfoSpacing: CGFloat = 8.0
let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing
let titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((clearBounds.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0 - leftInset, height: size.height))
titleSize.width += credibilityIconWidth
let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .left)
let titleInfoSpacing: CGFloat = 0.0
var titleFrame: CGRect
if activitySize.height.isZero {
titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize)
} else {
let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing
if let image = self.titleLeftIconNode.image {
self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size)
}
if let image = self.titleCredibilityIconNode.image {
self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size)
}
if let image = self.titleRightIconNode.image {
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size)
}
titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
var activityFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize)
self.activityNode.frame = activityFrame
}
/*if let networkStatusNode = self.networkStatusNode {
transition.updateFrame(node: networkStatusNode, frame: CGRect(origin: CGPoint(), size: size))
networkStatusNode.updateLayout(size: size, transition: transition)
}*/
if let image = self.titleLeftIconNode.image {
self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size)
}
if let image = self.titleCredibilityIconNode.image {
self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 2.0), size: image.size)
}
if let image = self.titleRightIconNode.image {
self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size)
}
}
@objc func buttonPressed() {

View File

@ -658,7 +658,7 @@ final class ListMessageFileItemNode: ListMessageNode {
strongSelf.addSubnode(waveformScrubbingNode)
}
strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - (leftOffset + leftInset) - 16.0, height: 12.0))
transition.updateFrame(node: waveformScrubbingNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - leftInset - 16.0, height: 12.0)))
waveformNode.setup(color: item.theme.list.controlSecondaryColor, waveform: waveform)
waveformForegroundNode.setup(color: item.theme.list.itemAccentColor, waveform: waveform)
@ -809,7 +809,7 @@ final class ListMessageFileItemNode: ListMessageNode {
}
}
self.waveformScrubbingNode?.enableScrubbing = enableScrubbing
if let musicIsPlaying = musicIsPlaying, !isVoice {
if let musicIsPlaying = musicIsPlaying, !isVoice, !isInstantVideo {
if self.playbackOverlayNode == nil {
let playbackOverlayNode = ListMessagePlaybackOverlayNode()
playbackOverlayNode.frame = self.iconImageNode.frame

View File

@ -35,24 +35,24 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> ListViewItem {
func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
let peer = self.peer
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: { node, gesture in
//arguments.contextAction(peer, node, gesture)
openPeerContextAction(peer, node, gesture)
}, hasTopStripe: false, noInsets: true)
}
}
private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> GroupsInCommonListTransaction {
private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) }
return GroupsInCommonListTransaction(deletions: deletions, insertions: insertions, updates: updates)
}
@ -60,7 +60,8 @@ private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to
final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let peerId: PeerId
private let paneInteraction: PeerInfoPaneInteraction
private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void
private let listNode: ListView
private var peers: [Peer] = []
@ -75,10 +76,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
return self.ready.get()
}
init(context: AccountContext, peerId: PeerId, interaction: PeerInfoPaneInteraction, peers: [Peer]) {
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, peers: [Peer]) {
self.context = context
self.peerId = peerId
self.paneInteraction = interaction
self.chatControllerInteraction = chatControllerInteraction
self.openPeerContextAction = openPeerContextAction
self.listNode = ListView()
@ -102,14 +104,14 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
}
}
func update(size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), headerInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.scrollEnabled = !isScrollingLockedAtTop
@ -124,7 +126,9 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer))
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in
self?.paneInteraction.openPeer(peer)
self?.chatControllerInteraction.openPeer(peer.id, .default, nil)
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(peer, node, gesture)
})
self.currentEntries = entries
self.enqueuedTransactions.append(transaction)
@ -156,13 +160,22 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
return nil
}
func updateHiddenMedia() {
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func addToTransitionSurface(view: UIView) {
}
func updateSelectedMessages(animated: Bool) {
}
}

View File

@ -0,0 +1,125 @@
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 chatControllerInteraction: ChatControllerInteraction
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 let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
private var selectedMessages: Set<MessageId>? {
didSet {
if self.selectedMessages != oldValue {
self.selectedMessagesPromise.set(.single(self.selectedMessages))
}
}
}
private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
self.context = context
self.peerId = peerId
self.chatControllerInteraction = chatControllerInteraction
self.selectedMessages = chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
self.selectedMessagesPromise.set(.single(self.selectedMessages))
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false))
super.init()
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, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, 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(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
self.listNode.scrollEnabled = !isScrollingLockedAtTop
}
func findLoadedMessage(id: MessageId) -> Message? {
self.listNode.messageInCurrentHistoryView(id)
}
func updateHiddenMedia() {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListMessageNode {
itemNode.updateHiddenMedia()
}
}
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
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
}
func addToTransitionSurface(view: UIView) {
self.view.addSubview(view)
}
func updateSelectedMessages(animated: Bool) {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
}
}
self.selectedMessages = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
}
}

View File

@ -16,17 +16,20 @@ private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white
private final class VisualMediaItemInteraction {
let openMessage: (MessageId) -> Void
let toggleSelection: (MessageId) -> Void
let openMessage: (Message) -> Void
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
let toggleSelection: (MessageId, Bool) -> Void
var hiddenMedia: [MessageId: [Media]] = [:]
var selectedMessageIds: Set<MessageId>?
init(
openMessage: @escaping (MessageId) -> Void,
toggleSelection: @escaping (MessageId) -> Void
openMessage: @escaping (Message) -> Void,
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
toggleSelection: @escaping (MessageId, Bool) -> Void
) {
self.openMessage = openMessage
self.openMessageContextActions = openMessageContextActions
self.toggleSelection = toggleSelection
}
}
@ -68,7 +71,12 @@ private final class VisualMediaItemNode: ASDisplayNode {
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.mediaBadgeNode)
self.containerNode.isGestureEnabled = false
self.containerNode.activated = { [weak self] gesture in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
strongSelf.interaction.openMessageContextActions(item.0.message, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
}
}
deinit {
@ -79,13 +87,17 @@ private final class VisualMediaItemNode: ASDisplayNode {
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.view.addGestureRecognizer(TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
@objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if case .ended = recognizer.state {
if let (item, _, _, _) = self.item {
self.interaction.openMessage(item.message.id)
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
if case .tap = gesture {
if let (item, _, _, _) = self.item {
self.interaction.openMessage(item.message)
}
}
}
}
}
@ -132,8 +144,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
let isStreamable = isMediaStreamable(message: item.message, media: file)
let statusState: RadialStatusNodeState
if isStreamable {
let statusState: RadialStatusNodeState = .none
/*if isStreamable {
statusState = .none
} else {
switch status {
@ -145,7 +157,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
case .Remote:
statusState = .download(.white)
}
}
}*/
switch statusState {
case .none:
@ -234,7 +246,11 @@ private final class VisualMediaItemNode: ASDisplayNode {
} else {
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
if let strongSelf = self, let messageId = strongSelf.item?.0.message.id {
strongSelf.interaction.toggleSelection(messageId)
var toggledValue = true
if let selectedMessageIds = strongSelf.interaction.selectedMessageIds, selectedMessageIds.contains(messageId) {
toggledValue = false
}
strongSelf.interaction.toggleSelection(messageId, toggledValue)
}
})
@ -305,7 +321,7 @@ private final class VisualMediaItem {
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
private let context: AccountContext
private let peerId: PeerId
private let interaction: PeerInfoPaneInteraction
private let chatControllerInteraction: ChatControllerInteraction
private let scrollNode: ASScrollNode
@ -314,7 +330,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return self._itemInteraction!
}
private var currentParams: (size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
@ -334,24 +350,27 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
private var decelerationAnimator: ConstantDisplayLinkAnimator?
init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, interaction: PeerInfoPaneInteraction) {
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId) {
self.context = context
self.peerId = peerId
self.interaction = interaction
self.chatControllerInteraction = chatControllerInteraction
self.scrollNode = ASScrollNode()
super.init()
self._itemInteraction = VisualMediaItemInteraction(
openMessage: { id in
openMessage(id)
openMessage: { [weak self] message in
self?.chatControllerInteraction.openMessage(message, .default)
},
toggleSelection: { id in
interaction.toggleMessageSelected(id)
openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in
self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture)
},
toggleSelection: { [weak self] id, value in
self?.chatControllerInteraction.toggleMessagesSelection([id], value)
}
)
self.itemInteraction.selectedMessageIds = self.interaction.selectedMessageIds
self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
self.scrollNode.view.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) {
@ -416,8 +435,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
let wasFirstHistoryView = self.isFirstHistoryView
self.isFirstHistoryView = false
if let (size, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams {
self.update(size: size, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams {
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
@ -444,6 +463,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return nil
}
func updateHiddenMedia() {
for (_, itemNode) in self.visibleMediaItems {
itemNode.updateHiddenMedia()
}
}
func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
//print("transferVelocity \(velocity)")
@ -493,15 +518,19 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return nil
}
func addToTransitionSurface(view: UIView) {
self.scrollNode.view.addSubview(view)
}
func updateSelectedMessages(animated: Bool) {
self.itemInteraction.selectedMessageIds = self.interaction.selectedMessageIds
self.itemInteraction.selectedMessageIds = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
for (_, itemNode) in self.visibleMediaItems {
itemNode.updateSelectionState(animated: animated)
}
}
func update(size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, visibleHeight, isScrollingLockedAtTop, presentationData)
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
@ -510,10 +539,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
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
let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + bottomInset
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
self.updateVisibleItems(size: size, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: synchronous)
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: synchronous)
if isScrollingLockedAtTop {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size))
@ -527,8 +556,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let (size, visibleHeight, _, presentationData) = self.currentParams {
self.updateVisibleItems(size: size, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: false)
if let (size, sideInset, bottomInset, visibleHeight, _, presentationData) = self.currentParams {
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, 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 {
@ -539,10 +568,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
}
}
private func updateVisibleItems(size: CGSize, visibleHeight: CGFloat, theme: PresentationTheme, synchronousLoad: Bool) {
private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, synchronousLoad: Bool) {
let availableWidth = size.width - sideInset * 2.0
let itemSpacing: CGFloat = 1.0
let itemsInRow: Int = max(3, min(6, Int(size.width / 140.0)))
let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow))
let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0)))
let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow))
let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1)
@ -562,8 +593,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
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 itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (availableWidth - itemOrigin.x) : itemSize, height: itemSize))
let itemNode: VisualMediaItemNode
if let current = self.visibleMediaItems[stableId] {
itemNode = current

View File

@ -0,0 +1,306 @@
import Foundation
import UIKit
import Postbox
import SyncCore
import TelegramCore
import SwiftSignalKit
import AccountContext
import PeerPresenceStatusManager
import TelegramStringFormatting
import TelegramPresentationData
final class PeerInfoState {
let isEditing: Bool
let isSearching: Bool
let selectedMessageIds: Set<MessageId>?
init(
isEditing: Bool,
isSearching: Bool,
selectedMessageIds: Set<MessageId>?
) {
self.isEditing = isEditing
self.isSearching = isSearching
self.selectedMessageIds = selectedMessageIds
}
func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
return PeerInfoState(
isEditing: isEditing,
isSearching: self.isSearching,
selectedMessageIds: self.selectedMessageIds
)
}
func withSelectedMessageIds(_ selectedMessageIds: Set<MessageId>?) -> PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
isSearching: self.isSearching,
selectedMessageIds: selectedMessageIds
)
}
}
final class PeerInfoScreenData {
let peer: Peer?
let cachedData: CachedPeerData?
let status: PeerInfoStatusData?
let notificationSettings: TelegramPeerNotificationSettings?
let globalNotificationSettings: GlobalNotificationSettings?
let isContact: Bool
let availablePanes: [PeerInfoPaneKey]
let groupsInCommon: [Peer]?
init(
peer: Peer?,
cachedData: CachedPeerData?,
status: PeerInfoStatusData?,
notificationSettings: TelegramPeerNotificationSettings?,
globalNotificationSettings: GlobalNotificationSettings?,
isContact: Bool,
availablePanes: [PeerInfoPaneKey],
groupsInCommon: [Peer]?
) {
self.peer = peer
self.cachedData = cachedData
self.status = status
self.notificationSettings = notificationSettings
self.globalNotificationSettings = globalNotificationSettings
self.isContact = isContact
self.availablePanes = availablePanes
self.groupsInCommon = groupsInCommon
}
}
enum PeerInfoScreenInputData: Equatable {
case none
case user(userId: PeerId, secretChatId: PeerId?, isBot: Bool)
}
func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey], NoError> {
let tags: [(MessageTags, PeerInfoPaneKey)] = [
(.photoOrVideo, .media),
(.file, .files),
(.music, .music),
(.voiceOrInstantVideo, .voice),
(.webPage, .links)
]
return combineLatest(tags.map { tagAndKey -> Signal<PeerInfoPaneKey?, NoError> in
let (tag, key) = tagAndKey
return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 20, clipHoles: false, fixedCombinedReadStates: nil, tagMask: tag)
|> map { (view, _, _) -> PeerInfoPaneKey? in
if view.entries.isEmpty {
return nil
} else {
return key
}
}
})
|> map { keys -> [PeerInfoPaneKey] in
return keys.compactMap { $0 }
}
|> distinctUntilChanged
/*return context.account.postbox.combinedView(keys: tags.map { (tag, _) -> PostboxViewKey in
return .historyTagInfo(peerId: peerId, tag: tag)
})
|> map { view -> [PeerInfoPaneKey] in
return tags.compactMap { (tag, key) -> PeerInfoPaneKey? in
if let info = view.views[.historyTagInfo(peerId: peerId, tag: tag)] as? HistoryTagInfoView, !info.isEmpty {
return key
} else {
return nil
}
}
}
|> distinctUntilChanged*/
}
struct PeerInfoStatusData: Equatable {
var text: String
var isActivity: Bool
}
func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> Signal<PeerInfoScreenData, NoError> {
return context.account.postbox.combinedView(keys: [.basicPeer(peerId)])
|> map { view -> PeerInfoScreenInputData in
guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else {
return .none
}
if let user = peer as? TelegramUser {
return .user(userId: user.id, secretChatId: nil, isBot: user.botInfo != nil)
} else {
preconditionFailure()
}
}
|> distinctUntilChanged
|> mapToSignal { inputData -> Signal<PeerInfoScreenData, NoError> in
switch inputData {
case .none:
return .single(PeerInfoScreenData(
peer: nil,
cachedData: nil,
status: nil,
notificationSettings: nil,
globalNotificationSettings: nil,
isContact: false,
availablePanes: [],
groupsInCommon: nil
))
case let .user(peerId, secretChatId, isBot):
let groupsInCommonSignal: Signal<[Peer]?, NoError>
if isBot {
groupsInCommonSignal = .single([])
} else {
groupsInCommonSignal = .single(nil)
|> then(
groupsInCommon(account: context.account, peerId: peerId)
|> map(Optional.init)
)
}
enum StatusInputData: Equatable {
case none
case presence(TelegramUserPresence)
case bot
}
let status = Signal<PeerInfoStatusData?, NoError> { subscriber in
class Manager {
var currentValue: TelegramUserPresence? = nil
var updateManager: QueueLocalObject<PeerPresenceStatusManager>? = nil
}
let manager = Atomic<Manager>(value: Manager())
let notify: () -> Void = {
let data = manager.with { manager -> PeerInfoStatusData? in
if let presence = manager.currentValue {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
let (text, isActivity) = stringAndActivityForUserPresence(strings: strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp), expanded: true)
return PeerInfoStatusData(text: text, isActivity: isActivity)
} else {
return nil
}
}
subscriber.putNext(data)
}
let disposable = (context.account.viewTracker.peerView(peerId, updateData: false)
|> map { view -> StatusInputData in
guard let user = view.peers[peerId] as? TelegramUser else {
return .none
}
if user.isDeleted {
return .none
}
if user.botInfo != nil {
return .bot
}
if user.flags.contains(.isSupport) {
return .none
}
guard let presence = view.peerPresences[peerId] as? TelegramUserPresence else {
return .none
}
return .presence(presence)
}
|> distinctUntilChanged).start(next: { inputData in
switch inputData {
case .bot:
subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericBotStatus, isActivity: false))
default:
var presence: TelegramUserPresence?
if case let .presence(value) = inputData {
presence = value
}
let _ = manager.with { manager -> Void in
manager.currentValue = presence
if let presence = presence {
let updateManager: QueueLocalObject<PeerPresenceStatusManager>
if let current = manager.updateManager {
updateManager = current
} else {
updateManager = QueueLocalObject<PeerPresenceStatusManager>(queue: .mainQueue(), generate: {
return PeerPresenceStatusManager(update: {
notify()
})
})
}
updateManager.with { updateManager in
updateManager.reset(presence: presence)
}
} else if let _ = manager.updateManager {
manager.updateManager = nil
}
}
notify()
}
})
return disposable
}
|> distinctUntilChanged
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
var combinedKeys: [PostboxViewKey] = []
combinedKeys.append(globalNotificationsKey)
if let secretChatId = secretChatId {
combinedKeys.append(.peerChatState(peerId: peerId))
}
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId),
context.account.postbox.combinedView(keys: combinedKeys),
status,
groupsInCommonSignal
)
|> map { peerView, availablePanes, combinedView, status, groupsInCommon -> PeerInfoScreenData in
var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
globalNotificationSettings = settings
}
}
var availablePanes = availablePanes
if let groupsInCommon = groupsInCommon, !groupsInCommon.isEmpty {
availablePanes.append(.groupsInCommon)
}
return PeerInfoScreenData(
peer: peerView.peers[peerId],
cachedData: peerView.cachedData,
status: status,
notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings,
globalNotificationSettings: globalNotificationSettings,
isContact: peerView.peerIsContact,
availablePanes: availablePanes,
groupsInCommon: groupsInCommon
)
}
}
}
}
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?) -> [PeerInfoHeaderButtonKey] {
var result: [PeerInfoHeaderButtonKey] = []
if let user = peer as? TelegramUser {
result.append(.message)
var callsAvailable = false
if !user.isDeleted, user.botInfo == nil, !user.flags.contains(.isSupport), let cachedUserData = cachedData as? CachedUserData {
callsAvailable = cachedUserData.callsAvailable
}
if callsAvailable {
result.append(.call)
}
result.append(.mute)
if !user.isDeleted, user.botInfo == nil && !user.flags.contains(.isSupport) {
result.append(.more)
}
}
return result
}
func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?) -> Bool {
if let user = peer as? TelegramUser {
if user.isDeleted {
return false
}
return true
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,553 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import Postbox
import SyncCore
import TelegramCore
import AccountContext
import ContextUI
protocol PeerInfoPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func scrollToTop() -> Bool
func transferVelocity(_ velocity: CGFloat)
func findLoadedMessage(id: MessageId) -> Message?
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
func addToTransitionSurface(view: UIView)
func updateHiddenMedia()
func updateSelectedMessages(animated: Bool)
}
final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey
let node: PeerInfoPaneNode
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)?
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
self.key = key
self.node = node
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams {
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData {
return
}
}
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData)
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition)
}
}
enum PeerInfoPaneKey {
case media
case files
case links
case voice
case music
case groupsInCommon
}
final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
private let pressed: () -> Void
private let titleNode: ImmediateTextNode
private let buttonNode: HighlightTrackingButtonNode
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.addSubnode(self.titleNode)
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.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.4
} else {
strongSelf.titleNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
@objc private func buttonPressed() {
self.pressed()
}
func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
}
func updateLayout(height: CGFloat) -> CGFloat {
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
return titleSize.width
}
func updateArea(size: CGSize, sideInset: CGFloat) {
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
}
}
struct PeerInfoPaneSpecifier: Equatable {
var key: PeerInfoPaneKey
var title: String
}
final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
private let scrollNode: ASScrollNode
private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:]
private let selectedLineNode: ASImageNode
private var currentParams: ([PeerInfoPaneSpecifier], PeerInfoPaneKey?, PresentationData)?
var requestSelectPane: ((PeerInfoPaneKey) -> Void)?
override init() {
self.scrollNode = ASScrollNode()
self.selectedLineNode = ASImageNode()
self.selectedLineNode.displaysAsynchronously = false
self.selectedLineNode.displayWithoutProcessing = true
super.init()
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.selectedLineNode)
}
func update(size: CGSize, presentationData: PresentationData, paneList: [PeerInfoPaneSpecifier], selectedPane: PeerInfoPaneKey?, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
let focusOnSelectedPane = self.currentParams?.1 != selectedPane
if self.currentParams?.2.theme !== presentationData.theme {
self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
})?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1)
}
if self.currentParams?.0 != paneList || self.currentParams?.1 != selectedPane || self.currentParams?.2 !== presentationData {
self.currentParams = (paneList, selectedPane, presentationData)
for specifier in paneList {
let paneNode: PeerInfoPaneTabsContainerPaneNode
var wasAdded = false
if let current = self.paneNodes[specifier.key] {
paneNode = current
} else {
wasAdded = true
paneNode = PeerInfoPaneTabsContainerPaneNode(pressed: { [weak self] in
self?.paneSelected(specifier.key)
})
self.paneNodes[specifier.key] = paneNode
self.scrollNode.addSubnode(paneNode)
}
paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData)
}
var removeKeys: [PeerInfoPaneKey] = []
for (key, _) in self.paneNodes {
if !paneList.contains(where: { $0.key == key }) {
removeKeys.append(key)
}
}
for key in removeKeys {
if let paneNode = self.paneNodes.removeValue(forKey: key) {
paneNode.removeFromSupernode()
}
}
}
var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode)] = []
var totalRawTabSize: CGFloat = 0.0
var selectedFrame: CGRect?
for specifier in paneList {
guard let paneNode = self.paneNodes[specifier.key] else {
continue
}
let paneNodeWidth = paneNode.updateLayout(height: size.height)
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
tabSizes.append((paneNodeSize, paneNode))
totalRawTabSize += paneNodeSize.width
}
let spacing: CGFloat = 32.0
if tabSizes.count == 1 {
for i in 0 ..< tabSizes.count {
let (paneNodeSize, paneNode) = tabSizes[i]
let leftOffset: CGFloat = 16.0
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
paneNode.frame = paneFrame
let areaSideInset: CGFloat = 16.0
paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset)
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset)
if paneList[i].key == selectedPane {
selectedFrame = paneFrame
}
}
self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height)
} else if totalRawTabSize + CGFloat(tabSizes.count + 1) * spacing <= size.width {
let availableSpace = size.width
let availableSpacing = availableSpace - totalRawTabSize
let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1))
var leftOffset = perTabSpacing
for i in 0 ..< tabSizes.count {
let (paneNodeSize, paneNode) = tabSizes[i]
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
paneNode.frame = paneFrame
let areaSideInset = floor(perTabSpacing / 2.0)
paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset)
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset)
leftOffset += paneNodeSize.width + perTabSpacing
if paneList[i].key == selectedPane {
selectedFrame = paneFrame
}
}
self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height)
} else {
let sideInset: CGFloat = 16.0
var leftOffset: CGFloat = sideInset
for i in 0 ..< tabSizes.count {
let (paneNodeSize, paneNode) = tabSizes[i]
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
paneNode.frame = paneFrame
paneNode.updateArea(size: paneFrame.size, sideInset: spacing)
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing, bottom: 0.0, right: -spacing)
if paneList[i].key == selectedPane {
selectedFrame = paneFrame
}
leftOffset += paneNodeSize.width + spacing
}
self.scrollNode.view.contentSize = CGSize(width: leftOffset + sideInset, height: size.height)
}
if let selectedFrame = selectedFrame {
self.selectedLineNode.isHidden = false
transition.updateFrame(node: self.selectedLineNode, frame: CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 4.0), size: CGSize(width: selectedFrame.width, height: 4.0)))
if focusOnSelectedPane {
if selectedPane == paneList.first?.key {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size))
} else if selectedPane == paneList.last?.key {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, y: 0.0), size: self.scrollNode.bounds.size))
} else {
let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size))
}
}
} else {
self.selectedLineNode.isHidden = true
}
}
private func paneSelected(_ key: PeerInfoPaneKey) {
self.requestSelectPane?(key)
}
}
final class PeerInfoPaneContainerNode: ASDisplayNode {
private let context: AccountContext
private let peerId: PeerId
private let coveringBackgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let tabsContainerNode: PeerInfoPaneTabsContainerNode
private let tapsSeparatorNode: ASDisplayNode
let isReady = Promise<Bool>()
var didSetIsReady = false
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)?
private(set) var currentPaneKey: PeerInfoPaneKey?
private(set) var currentPane: PeerInfoPaneWrapper?
private var currentCandidatePaneKey: PeerInfoPaneKey?
private var candidatePane: (PeerInfoPaneWrapper, Disposable, Bool)?
var selectionPanelNode: PeerInfoSelectionPanelNode?
var chatControllerInteraction: ChatControllerInteraction?
var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?
var currentPaneUpdated: (() -> Void)?
private var currentAvailablePanes: [PeerInfoPaneKey]?
init(context: AccountContext, peerId: PeerId) {
self.context = context
self.peerId = peerId
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.coveringBackgroundNode = ASDisplayNode()
self.coveringBackgroundNode.isLayerBacked = true
self.tabsContainerNode = PeerInfoPaneTabsContainerNode()
self.tapsSeparatorNode = ASDisplayNode()
self.tapsSeparatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.separatorNode)
self.addSubnode(self.coveringBackgroundNode)
self.addSubnode(self.tabsContainerNode)
self.addSubnode(self.tapsSeparatorNode)
self.tabsContainerNode.requestSelectPane = { [weak self] key in
guard let strongSelf = self else {
return
}
if strongSelf.currentPaneKey == key {
strongSelf.currentPane?.node.scrollToTop()
return
}
if strongSelf.currentCandidatePaneKey == key {
return
}
strongSelf.currentCandidatePaneKey = key
if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate)
}
}
}
func scrollToTop() -> Bool {
if let currentPane = self.currentPane {
return currentPane.node.scrollToTop()
} else {
return false
}
}
func findLoadedMessage(id: MessageId) -> Message? {
return self.currentPane?.node.findLoadedMessage(id: id)
}
func updateHiddenMedia() {
self.currentPane?.node.updateHiddenMedia()
}
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media)
}
func updateSelectedMessageIds(_ selectedMessageIds: Set<MessageId>?, animated: Bool) {
self.currentPane?.node.updateSelectedMessages(animated: animated)
self.candidatePane?.0.node.updateSelectedMessages(animated: animated)
}
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) {
let previousAvailablePanes = self.currentAvailablePanes ?? []
let availablePanes = data?.availablePanes ?? []
self.currentAvailablePanes = availablePanes
if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) {
var nextCandidatePaneKey: PeerInfoPaneKey?
if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 {
for i in (0 ... index - 1).reversed() {
if availablePanes.contains(previousAvailablePanes[i]) {
nextCandidatePaneKey = previousAvailablePanes[i]
}
}
}
if nextCandidatePaneKey == nil {
nextCandidatePaneKey = availablePanes.first
}
if let nextCandidatePaneKey = nextCandidatePaneKey {
if self.currentCandidatePaneKey != nextCandidatePaneKey {
self.currentCandidatePaneKey = nextCandidatePaneKey
}
} else {
self.currentCandidatePaneKey = nil
if let (_, disposable, _) = self.candidatePane {
disposable.dispose()
self.candidatePane = nil
}
if let currentPane = self.currentPane {
self.currentPane = nil
currentPane.node.removeFromSupernode()
}
}
} else if self.currentPaneKey == nil {
self.currentCandidatePaneKey = availablePanes.first
}
let previousCurrentPaneKey = self.currentPaneKey
self.currentParams = (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data)
transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction)
self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.tapsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let tabsHeight: CGFloat = 48.0
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel)))
transition.updateFrame(node: self.tapsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight))
if let currentCandidatePaneKey = self.currentCandidatePaneKey {
if self.candidatePane?.0.key != currentCandidatePaneKey {
self.candidatePane?.1.dispose()
let paneNode: PeerInfoPaneNode
switch currentCandidatePaneKey {
case .media:
paneNode = PeerInfoVisualMediaPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId)
case .files:
paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .file)
case .links:
paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .webPage)
case .voice:
paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .voiceOrInstantVideo)
case .music:
paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .music)
case .groupsInCommon:
paneNode = PeerInfoGroupsInCommonPaneNode(context: self.context, peerId: peerId, chatControllerInteraction: self.chatControllerInteraction!, openPeerContextAction: self.openPeerContextAction!, peers: data?.groupsInCommon ?? [])
}
let disposable = MetaDisposable()
self.candidatePane = (PeerInfoPaneWrapper(key: currentCandidatePaneKey, node: paneNode), disposable, false)
var shouldReLayout = false
disposable.set((paneNode.isReady
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
if let (candidatePane, disposable, _) = strongSelf.candidatePane {
strongSelf.candidatePane = (candidatePane, disposable, true)
if shouldReLayout {
if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: strongSelf.currentPane != nil ? .animated(duration: 0.35, curve: .spring) : .immediate)
}
}
}
}))
shouldReLayout = true
}
}
if let (candidatePane, _, isReady) = self.candidatePane, isReady {
let previousPane = self.currentPane
self.candidatePane = nil
self.currentPaneKey = candidatePane.key
self.currentCandidatePaneKey = nil
self.currentPane = candidatePane
if let selectionPanelNode = self.selectionPanelNode {
self.insertSubnode(candidatePane.node, belowSubnode: selectionPanelNode)
} else {
self.addSubnode(candidatePane.node)
}
candidatePane.node.frame = paneFrame
candidatePane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: max(0.0, visibleHeight - paneFrame.minY), isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate)
if let previousPane = previousPane {
let directionToRight: Bool
if let previousIndex = availablePanes.index(of: previousPane.key), let updatedIndex = availablePanes.index(of: candidatePane.key) {
directionToRight = previousIndex < updatedIndex
} else {
directionToRight = false
}
let offset: CGFloat = directionToRight ? previousPane.node.bounds.width : -previousPane.node.bounds.width
transition.animatePositionAdditive(node: candidatePane.node, offset: CGPoint(x: offset, y: 0.0))
let previousNode = previousPane.node
transition.updateFrame(node: previousNode, frame: paneFrame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak previousNode] _ in
previousNode?.removeFromSupernode()
})
}
} else if let currentPane = self.currentPane {
let paneWasAdded = currentPane.node.supernode == nil
if paneWasAdded {
self.addSubnode(currentPane.node)
}
let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : transition
paneTransition.updateFrame(node: currentPane.node, frame: paneFrame)
currentPane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
}
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight)))
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String
switch key {
case .media:
title = "Media"
case .files:
title = "Files"
case .links:
title = "Links"
case .voice:
title = "Voice Messages"
case .music:
title = "Audio"
case .groupsInCommon:
title = "Groups"
}
return PeerInfoPaneSpecifier(key: key, title: title)
}, selectedPane: self.currentPaneKey, transition: transition)
if let (candidatePane, _, _) = self.candidatePane {
let paneTransition: ContainedViewLayoutTransition = .immediate
paneTransition.updateFrame(node: candidatePane.node, frame: paneFrame)
candidatePane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition)
}
if !self.didSetIsReady && data != nil {
if let currentPane = self.currentPane {
self.didSetIsReady = true
self.isReady.set(currentPane.node.isReady)
} else if self.candidatePane == nil {
self.didSetIsReady = true
self.isReady.set(.single(true))
}
}
if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey {
self.currentPaneUpdated?()
}
}
}

View File

@ -1,217 +0,0 @@
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 paneInteraction: PeerInfoPaneInteraction
private let controllerInteraction: ChatControllerInteraction
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 let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
private var selectedMessages: Set<MessageId>? {
didSet {
if self.selectedMessages != oldValue {
self.selectedMessagesPromise.set(.single(self.selectedMessages))
}
}
}
private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, tagMask: MessageTags, interaction: PeerInfoPaneInteraction) {
self.context = context
self.peerId = peerId
self.paneInteraction = interaction
var openMessageImpl: ((MessageId) -> Bool)?
var toggleMessageSelectionImpl: (([MessageId]) -> Void)?
self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in
return openMessageImpl?(message.id) ?? false
}, openPeer: { _, _, _ in
}, openPeerMention: { _ in
}, openMessageContextMenu: { _, _, _, _, _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in
}, tapMessage: nil, clickThroughMessage: {
}, toggleMessagesSelection: { ids, _ in
toggleMessageSelectionImpl?(ids)
}, 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.controllerInteraction.selectionState = self.paneInteraction.selectedMessageIds.flatMap { ids in
return ChatInterfaceSelectionState(selectedIds: ids)
}
self.selectedMessages = self.paneInteraction.selectedMessageIds
self.selectedMessagesPromise.set(.single(self.selectedMessages))
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false))
super.init()
openMessageImpl = { id in
return openMessage(id)
}
toggleMessageSelectionImpl = { [weak self] ids in
for id in ids {
self?.paneInteraction.toggleMessageSelected(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]
}
}
strongSelf.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, visibleHeight: CGFloat, 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 transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
self.listNode.transferVelocity(velocity)
}
}
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
}
func updateSelectedMessages(animated: Bool) {
self.controllerInteraction.selectionState = self.paneInteraction.selectedMessageIds.flatMap { ids in
return ChatInterfaceSelectionState(selectedIds: ids)
}
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
}
}
self.selectedMessages = self.paneInteraction.selectedMessageIds
}
}